using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using LLPlayer.Services;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = ((App)Application.Current).Container.Resolve<MainWindowVM>();
        InitializeComponent();
        SetWindowSize();
        SetTitleBarDarkMode(this);
    }
    private void SetWindowSize()
    {
        List<Size> candidateSizes =
        [
            new(1280, 720),
            new(1024, 576),
            new(960, 540),
            new(800, 450),
            new(640, 360),
            new(480, 270),
            new(320, 180)
        ];
        double availableWidth = SystemParameters.WorkArea.Width;
        double availableHeight = SystemParameters.WorkArea.Height;
        Size selectedSize = candidateSizes.FirstOrDefault(
            s => s.Width <= availableWidth && s.Height <= availableHeight,
            candidateSizes[^1]);
        Width = selectedSize.Width;
        Height = selectedSize.Height;
    }
    #region Dark Title Bar
    [DllImport("DwmApi")]
    private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);
    const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
    public static void SetTitleBarDarkMode(Window window)
    {
        if (!(Environment.OSVersion.Version >= new Version(10, 0, 18985)))
        {
            return;
        }
        var fl = ((App)Application.Current).Container.Resolve<FlyleafManager>();
        if (!fl.Config.IsDarkTitlebar)
        {
            return;
        }
        bool darkMode = true;
        IntPtr hWnd = new WindowInteropHelper(window).EnsureHandle();
        DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, [darkMode ? 1 : 0], 4);
    }
    #endregion
}

using System;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Input;
using System.Windows.Interop;

using FlyleafLib.MediaPlayer;

using static FlyleafLib.Utils;
using static FlyleafLib.Utils.NativeMethods;

using Brushes = System.Windows.Media.Brushes;

namespace FlyleafLib.Controls.WPF;

public class FlyleafHost : ContentControl, IHostPlayer, IDisposable
{
    /* -= FlyleafHost Properties Notes =-

        Player							[Can be changed, can be null]
        ReplicaPlayer                                                       | Replicates frames of the assigned Player (useful for interactive zoom) without the pan/zoom config

        Surface							[ReadOnly / Required]
        Overlay							[AutoCreated OnContentChanged       | Provided directly | Provided in Stand Alone Constructor]

        Content							[Overlay's Content]
        DetachedContent					[Host's actual content]

        DataContext						[Set by the user or default inheritance]
        HostDataContext					[Will be set Sync with DataContext as helper to Overlay when we pass this as Overlay's DataContext]

        OpenOnDrop						[None, Surface, Overlay, Both]		| Requires Player and AllowDrop
        SwapOnDrop						[None, Surface, Overlay, Both]		| Requires Player and AllowDrop

        SwapDragEnterOnShift			[None, Surface, Overlay, Both]		| Requires Player and SwapOnDrop
        ToggleFullScreenOnDoubleClick	[None, Surface, Overlay, Both]

        PanMoveOnCtrl					[None, Surface, Overlay, Both]		| Requires Player and VideoStream Opened
        PanZoomOnCtrlWheel			    [None, Surface, Overlay, Both]		| Requires Player and VideoStream Opened
        PanRotateOnShiftWheel           [None, Surface, Overlay, Both]      | Requires Player and VideoStream Opened

        AttachedDragMove				[None, Surface, Overlay, Both, SurfaceOwner, OverlayOwner, BothOwner]
        DetachedDragMove				[None, Surface, Overlay, Both]

        AttachedResize					[None, Surface, Overlay, Both]
        DetachedResize					[None, Surface, Overlay, Both]
        KeepRatioOnResize				[False, True]
        PreferredLandscapeWidth         [X]                                 | When KeepRatioOnResize will use it as helper and try to stay close to this value (CurResizeRatio >= 1) - Will be updated when user resizes a landscape
        PreferredPortraitHeight         [Y]                                 | When KeepRatioOnResize will use it as helper and try to stay close to this value (CurResizeRatio <  1) - Will be updated when user resizes a portrait
        CurResizeRatio                  [0 if not Keep Ratio or Player's aspect ratio]
        ResizeSensitivity               Pixels sensitivity from the window's edges

        BringToFrontOnClick             [False, True]

        DetachedPosition				[Custom, TopLeft, TopCenter, TopRight, CenterLeft, CenterCenter, CenterRight, BottomLeft, BottomCenter, BottomRight]
        DetachedPositionMargin			[X, Y, CX, CY]						| Does not affect the Size / Eg. No point to provide both X/CX
        DetachedFixedPosition			[X, Y]								| if remember only first time
        DetachedFixedSize				[CX, CY]							| if remember only first time
        DetachedRememberPosition		[False, True]
        DetachedRememberSize			[False, True]
        DetachedTopMost					[False, True] (Surfaces Only Required?)
        DetachedShowInTaskbar           [False, True]                       | When Detached or Fullscreen will be in Switch Apps
        DetachedNoOwner                 [False, True]                       | When Detached will not follow the owner's window state (Minimize/Maximize)

        KeyBindings						[None, Surface, Overlay, Both]
        MouseBindings                   [None, Surface, Overlay, Both]      | Required for all other mouse events

        ActivityTimeout					[0: Disabled]						| Requires Player?
        ActivityRefresh?				[None, Surface, Overlay, Both]		| MouseMove / MouseDown / KeyUp

        PassWheelToOwner?				[None, Surface, Overlay, Both]		| When host belongs to ScrollViewer

        IsAttached                      [False, True]
        IsFullScreen                    [False, True]                       | Should be used instead of WindowStates
        IsMinimized                     [False, True]                       | Should be used instead of WindowStates
        IsResizing						[ReadOnly]
        IsSwapping						[ReadOnly]
        IsStandAlone					[ReadOnly]
     */

    /* TODO
     * 1) The surface / overlay events code is repeated
     * 2) PassWheelToOwner (Related with LayoutUpdate performance / ScrollViewer) / ActivityRefresh
     * 3) Attach to different Owner (Load/Unload) and change Overlay?
     * 4) WindowStates should not be used by user directly. Use IsMinimized and IsFullScreen instead.
     * 5) WS_EX_NOACTIVATE should be set but for some reason is not required (for none-styled windows)? Currently BringToFront does the job but (only for left clicks?)
     */

    #region Properties / Variables
    public Window       Owner           { get; private set; }
    public Window       Surface         { get; private set; }
    public IntPtr       SurfaceHandle   { get; private set; }
    public IntPtr       OverlayHandle   { get; private set; }
    public IntPtr       OwnerHandle     { get; private set; }
    public int          ResizingSide    { get; private set; }

    public int          UniqueId        { get; private set; }
    public bool         Disposed        { get; private set; }

    public event EventHandler SurfaceCreated;
    public event EventHandler OverlayCreated;
    public event DragEventHandler OnSurfaceDrop;
    public event DragEventHandler OnOverlayDrop;

    static bool isDesignMode;
    static int  idGenerator = 1;
    static nint NONE_STYLE = (nint) (WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_CLIPSIBLINGS | WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_VISIBLE); // WS_MINIMIZEBOX required for swapchain
    static Rect rectRandom = new(1, 2, 3, 4);

    float curResizeRatio;
    float curResizeRatioIfEnabled;
    bool surfaceClosed, surfaceClosing, overlayClosed;
    int panPrevX, panPrevY;
    bool isMouseBindingsSubscribedSurface;
    bool isMouseBindingsSubscribedOverlay;
    Window standAloneOverlay;

    CornerRadius zeroCornerRadius = new(0);
    Point zeroPoint = new(0, 0);
    Point mouseLeftDownPoint = new(0, 0);
    Point mouseMoveLastPoint = new(0, 0);
    Point ownerZeroPointPos = new();

    Rect zeroRect = new(0, 0, 0, 0);
    Rect rectDetachedLast = Rect.Empty;
    Rect rectInit;
    Rect rectInitLast = rectRandom;
    Rect rectIntersect;
    Rect rectIntersectLast = rectRandom;
    RECT beforeResizeRect = new();
    RECT curRect = new();

    private class FlyleafHostDropWrap { public FlyleafHost FlyleafHost; } // To allow non FlyleafHosts to drag & drop
    protected readonly LogHandler Log;
    #endregion

    #region Dependency Properties
    public void BringToFront() => SetWindowPos(SurfaceHandle, IntPtr.Zero, 0, 0, 0, 0, (UInt32)(SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOMOVE));
    public bool BringToFrontOnClick
    {
        get { return (bool)GetValue(BringToFrontOnClickProperty); }
        set { SetValue(BringToFrontOnClickProperty, value); }
    }
    public static readonly DependencyProperty BringToFrontOnClickProperty =
    DependencyProperty.Register(nameof(BringToFrontOnClick), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(true));

    public AvailableWindows OpenOnDrop
    {
        get => (AvailableWindows)GetValue(OpenOnDropProperty);
        set => SetValue(OpenOnDropProperty, value);
    }
    public static readonly DependencyProperty OpenOnDropProperty =
        DependencyProperty.Register(nameof(OpenOnDrop), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface, new PropertyChangedCallback(DropChanged)));

    public AvailableWindows SwapOnDrop
    {
        get => (AvailableWindows)GetValue(SwapOnDropProperty);
        set => SetValue(SwapOnDropProperty, value);
    }
    public static readonly DependencyProperty SwapOnDropProperty =
        DependencyProperty.Register(nameof(SwapOnDrop), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface, new PropertyChangedCallback(DropChanged)));

    public AvailableWindows SwapDragEnterOnShift
    {
        get => (AvailableWindows)GetValue(SwapDragEnterOnShiftProperty);
        set => SetValue(SwapDragEnterOnShiftProperty, value);
    }
    public static readonly DependencyProperty SwapDragEnterOnShiftProperty =
        DependencyProperty.Register(nameof(SwapDragEnterOnShift), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows ToggleFullScreenOnDoubleClick
    {
        get => (AvailableWindows)GetValue(ToggleFullScreenOnDoubleClickProperty);
        set => SetValue(ToggleFullScreenOnDoubleClickProperty, value);
    }
    public static readonly DependencyProperty ToggleFullScreenOnDoubleClickProperty =
        DependencyProperty.Register(nameof(ToggleFullScreenOnDoubleClick), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows PanMoveOnCtrl
    {
        get => (AvailableWindows)GetValue(PanMoveOnCtrlProperty);
        set => SetValue(PanMoveOnCtrlProperty, value);
    }
    public static readonly DependencyProperty PanMoveOnCtrlProperty =
        DependencyProperty.Register(nameof(PanMoveOnCtrl), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows PanRotateOnShiftWheel
    {
        get => (AvailableWindows)GetValue(PanRotateOnShiftWheelProperty);
        set => SetValue(PanRotateOnShiftWheelProperty, value);
    }
    public static readonly DependencyProperty PanRotateOnShiftWheelProperty =
        DependencyProperty.Register(nameof(PanRotateOnShiftWheel), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows PanZoomOnCtrlWheel
    {
        get => (AvailableWindows)GetValue(PanZoomOnCtrlWheelProperty);
        set => SetValue(PanZoomOnCtrlWheelProperty, value);
    }
    public static readonly DependencyProperty PanZoomOnCtrlWheelProperty =
        DependencyProperty.Register(nameof(PanZoomOnCtrlWheel), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AttachedDragMoveOptions AttachedDragMove
    {
        get => (AttachedDragMoveOptions)GetValue(AttachedDragMoveProperty);
        set => SetValue(AttachedDragMoveProperty, value);
    }
    public static readonly DependencyProperty AttachedDragMoveProperty =
        DependencyProperty.Register(nameof(AttachedDragMove), typeof(AttachedDragMoveOptions), typeof(FlyleafHost), new PropertyMetadata(AttachedDragMoveOptions.Surface));

    public AvailableWindows DetachedDragMove
    {
        get => (AvailableWindows)GetValue(DetachedDragMoveProperty);
        set => SetValue(DetachedDragMoveProperty, value);
    }
    public static readonly DependencyProperty DetachedDragMoveProperty =
        DependencyProperty.Register(nameof(DetachedDragMove), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows AttachedResize
    {
        get => (AvailableWindows)GetValue(AttachedResizeProperty);
        set => SetValue(AttachedResizeProperty, value);
    }
    public static readonly DependencyProperty AttachedResizeProperty =
        DependencyProperty.Register(nameof(AttachedResize), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows DetachedResize
    {
        get => (AvailableWindows)GetValue(DetachedResizeProperty);
        set => SetValue(DetachedResizeProperty, value);
    }
    public static readonly DependencyProperty DetachedResizeProperty =
        DependencyProperty.Register(nameof(DetachedResize), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public bool KeepRatioOnResize
    {
        get => (bool)GetValue(KeepRatioOnResizeProperty);
        set => SetValue(KeepRatioOnResizeProperty, value);
    }
    public static readonly DependencyProperty KeepRatioOnResizeProperty =
        DependencyProperty.Register(nameof(KeepRatioOnResize), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnKeepRatioOnResizeChanged)));

    public int PreferredLandscapeWidth
    {
        get { return (int)GetValue(PreferredLandscapeWidthProperty); }
        set { SetValue(PreferredLandscapeWidthProperty, value); }
    }
    public static readonly DependencyProperty PreferredLandscapeWidthProperty =
    DependencyProperty.Register(nameof(PreferredLandscapeWidth), typeof(int), typeof(FlyleafHost), new PropertyMetadata(0));

    public int PreferredPortraitHeight
    {
        get { return (int)GetValue(PreferredPortraitHeightProperty); }
        set { SetValue(PreferredPortraitHeightProperty, value); }
    }
    public static readonly DependencyProperty PreferredPortraitHeightProperty =
    DependencyProperty.Register(nameof(PreferredPortraitHeight), typeof(int), typeof(FlyleafHost), new PropertyMetadata(0));

    public int ResizeSensitivity
    {
        get => (int)GetValue(ResizeSensitivityProperty);
        set => SetValue(ResizeSensitivityProperty, value);
    }
    public static readonly DependencyProperty ResizeSensitivityProperty =
        DependencyProperty.Register(nameof(ResizeSensitivity), typeof(int), typeof(FlyleafHost), new PropertyMetadata(6));

    public DetachedPositionOptions DetachedPosition
    {
        get => (DetachedPositionOptions)GetValue(DetachedPositionProperty);
        set => SetValue(DetachedPositionProperty, value);
    }
    public static readonly DependencyProperty DetachedPositionProperty =
        DependencyProperty.Register(nameof(DetachedPosition), typeof(DetachedPositionOptions), typeof(FlyleafHost), new PropertyMetadata(DetachedPositionOptions.CenterCenter));

    public Thickness DetachedPositionMargin
    {
        get => (Thickness)GetValue(DetachedPositionMarginProperty);
        set => SetValue(DetachedPositionMarginProperty, value);
    }
    public static readonly DependencyProperty DetachedPositionMarginProperty =
        DependencyProperty.Register(nameof(DetachedPositionMargin), typeof(Thickness), typeof(FlyleafHost), new PropertyMetadata(new Thickness(0, 0, 0, 0)));

    public Point DetachedFixedPosition
    {
        get => (Point)GetValue(DetachedFixedPositionProperty);
        set => SetValue(DetachedFixedPositionProperty, value);
    }
    public static readonly DependencyProperty DetachedFixedPositionProperty =
        DependencyProperty.Register(nameof(DetachedFixedPosition), typeof(Point), typeof(FlyleafHost), new PropertyMetadata(new Point()));

    public Size DetachedFixedSize
    {
        get => (Size)GetValue(DetachedFixedSizeProperty);
        set => SetValue(DetachedFixedSizeProperty, value);
    }
    public static readonly DependencyProperty DetachedFixedSizeProperty =
        DependencyProperty.Register(nameof(DetachedFixedSize), typeof(Size), typeof(FlyleafHost), new PropertyMetadata(new Size(300, 200)));

    public bool DetachedRememberPosition
    {
        get => (bool)GetValue(DetachedRememberPositionProperty);
        set => SetValue(DetachedRememberPositionProperty, value);
    }
    public static readonly DependencyProperty DetachedRememberPositionProperty =
        DependencyProperty.Register(nameof(DetachedRememberPosition), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(true));

    public bool DetachedRememberSize
    {
        get => (bool)GetValue(DetachedRememberSizeProperty);
        set => SetValue(DetachedRememberSizeProperty, value);
    }
    public static readonly DependencyProperty DetachedRememberSizeProperty =
        DependencyProperty.Register(nameof(DetachedRememberSize), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(true));

    public bool DetachedTopMost
    {
        get => (bool)GetValue(DetachedTopMostProperty);
        set => SetValue(DetachedTopMostProperty, value);
    }
    public static readonly DependencyProperty DetachedTopMostProperty =
        DependencyProperty.Register(nameof(DetachedTopMost), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnDetachedTopMostChanged)));

    public bool DetachedShowInTaskbar
    {
        get { return (bool)GetValue(DetachedShowInTaskbarProperty); }
        set { SetValue(DetachedShowInTaskbarProperty, value); }
    }
    public static readonly DependencyProperty DetachedShowInTaskbarProperty =
        DependencyProperty.Register(nameof(DetachedShowInTaskbar), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnShowInTaskBarChanged)));

    public bool DetachedNoOwner
    {
        get { return (bool)GetValue(DetachedNoOwnerProperty); }
        set { SetValue(DetachedNoOwnerProperty, value); }
    }
    public static readonly DependencyProperty DetachedNoOwnerProperty =
        DependencyProperty.Register(nameof(DetachedNoOwner), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnNoOwnerChanged)));

    public int DetachedMinHeight
    {
        get { return (int)GetValue(DetachedMinHeightProperty); }
        set { SetValue(DetachedMinHeightProperty, value); }
    }
    public static readonly DependencyProperty DetachedMinHeightProperty =
        DependencyProperty.Register(nameof(DetachedMinHeight), typeof(int), typeof(FlyleafHost), new PropertyMetadata(0));

    public int DetachedMinWidth
    {
        get { return (int)GetValue(DetachedMinWidthProperty); }
        set { SetValue(DetachedMinWidthProperty, value); }
    }
    public static readonly DependencyProperty DetachedMinWidthProperty =
        DependencyProperty.Register(nameof(DetachedMinWidth), typeof(int), typeof(FlyleafHost), new PropertyMetadata(0));

    public double DetachedMaxHeight
    {
        get { return (double)GetValue(DetachedMaxHeightProperty); }
        set { SetValue(DetachedMaxHeightProperty, value); }
    }
    public static readonly DependencyProperty DetachedMaxHeightProperty =
        DependencyProperty.Register(nameof(DetachedMaxHeight), typeof(double), typeof(FlyleafHost), new PropertyMetadata(double.PositiveInfinity));

    public double DetachedMaxWidth
    {
        get { return (double)GetValue(DetachedMaxWidthProperty); }
        set { SetValue(DetachedMaxWidthProperty, value); }
    }
    public static readonly DependencyProperty DetachedMaxWidthProperty =
        DependencyProperty.Register(nameof(DetachedMaxWidth), typeof(double), typeof(FlyleafHost), new PropertyMetadata(double.PositiveInfinity));

    public AvailableWindows KeyBindings
    {
        get => (AvailableWindows)GetValue(KeyBindingsProperty);
        set => SetValue(KeyBindingsProperty, value);
    }
    public static readonly DependencyProperty KeyBindingsProperty =
        DependencyProperty.Register(nameof(KeyBindings), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Surface));

    public AvailableWindows MouseBindings
    {
        get => (AvailableWindows)GetValue(MouseBindingsProperty);
        set => SetValue(MouseBindingsProperty, value);
    }
    public static readonly DependencyProperty MouseBindingsProperty =
        DependencyProperty.Register(nameof(MouseBindings), typeof(AvailableWindows), typeof(FlyleafHost), new PropertyMetadata(AvailableWindows.Both, new PropertyChangedCallback(OnMouseBindings)));

    public int ActivityTimeout
    {
        get => (int)GetValue(ActivityTimeoutProperty);
        set => SetValue(ActivityTimeoutProperty, value);
    }
    public static readonly DependencyProperty ActivityTimeoutProperty =
        DependencyProperty.Register(nameof(ActivityTimeout), typeof(int), typeof(FlyleafHost), new PropertyMetadata(0, new PropertyChangedCallback(OnActivityTimeoutChanged)));

    public bool IsAttached
    {
        get => (bool)GetValue(IsAttachedProperty);
        set => SetValue(IsAttachedProperty, value);
    }
    public static readonly DependencyProperty IsAttachedProperty =
        DependencyProperty.Register(nameof(IsAttached), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(true, new PropertyChangedCallback(OnIsAttachedChanged)));

    public bool IsMinimized
    {
        get => (bool)GetValue(IsMinimizedProperty);
        set => SetValue(IsMinimizedProperty, value);
    }
    public static readonly DependencyProperty IsMinimizedProperty =
        DependencyProperty.Register(nameof(IsMinimized), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnIsMinimizedChanged)));

    public bool IsFullScreen
    {
        get => (bool)GetValue(IsFullScreenProperty);
        set => SetValue(IsFullScreenProperty, value);
    }
    public static readonly DependencyProperty IsFullScreenProperty =
        DependencyProperty.Register(nameof(IsFullScreen), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false, new PropertyChangedCallback(OnIsFullScreenChanged)));

    public bool IsResizing
    {
        get => (bool)GetValue(IsResizingProperty);
        private set => SetValue(IsResizingProperty, value);
    }
    public static readonly DependencyProperty IsResizingProperty =
        DependencyProperty.Register(nameof(IsResizing), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public bool IsStandAlone
    {
        get => (bool)GetValue(IsStandAloneProperty);
        private set => SetValue(IsStandAloneProperty, value);
    }
    public static readonly DependencyProperty IsStandAloneProperty =
        DependencyProperty.Register(nameof(IsStandAlone), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public bool IsSwappingStarted
    {
        get => (bool)GetValue(IsSwappingStartedProperty);
        private set => SetValue(IsSwappingStartedProperty, value);
    }
    public static readonly DependencyProperty IsSwappingStartedProperty =
        DependencyProperty.Register(nameof(IsSwappingStarted), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public bool IsPanMoving
    {
        get { return (bool)GetValue(IsPanMovingProperty); }
        private set { SetValue(IsPanMovingProperty, value); }
    }
    public static readonly DependencyProperty IsPanMovingProperty =
        DependencyProperty.Register(nameof(IsPanMoving), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public bool IsDragMoving
    {
        get { return (bool)GetValue(IsDragMovingProperty); }
        set { SetValue(IsDragMovingProperty, value); }
    }
    public static readonly DependencyProperty IsDragMovingProperty =
        DependencyProperty.Register(nameof(IsDragMoving), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public bool IsDragMovingOwner
    {
        get { return (bool)GetValue(IsDragMovingOwnerProperty); }
        set { SetValue(IsDragMovingOwnerProperty, value); }
    }
    public static readonly DependencyProperty IsDragMovingOwnerProperty =
        DependencyProperty.Register(nameof(IsDragMovingOwner), typeof(bool), typeof(FlyleafHost), new PropertyMetadata(false));

    public FrameworkElement MarginTarget
    {
        get => (FrameworkElement)GetValue(MarginTargetProperty);
        set => SetValue(MarginTargetProperty, value);
    }
    public static readonly DependencyProperty MarginTargetProperty =
        DependencyProperty.Register(nameof(MarginTarget), typeof(FrameworkElement), typeof(FlyleafHost), new PropertyMetadata(null));

    public object HostDataContext
    {
        get => GetValue(HostDataContextProperty);
        set => SetValue(HostDataContextProperty, value);
    }
    public static readonly DependencyProperty HostDataContextProperty =
        DependencyProperty.Register(nameof(HostDataContext), typeof(object), typeof(FlyleafHost), new PropertyMetadata(null));

    public object DetachedContent
    {
        get => GetValue(DetachedContentProperty);
        set => SetValue(DetachedContentProperty, value);
    }
    public static readonly DependencyProperty DetachedContentProperty =
        DependencyProperty.Register(nameof(DetachedContent), typeof(object), typeof(FlyleafHost), new PropertyMetadata(null));

    public Player Player
    {
        get => (Player)GetValue(PlayerProperty);
        set => SetValue(PlayerProperty, value);
    }
    public static readonly DependencyProperty PlayerProperty =
        DependencyProperty.Register(nameof(Player), typeof(Player), typeof(FlyleafHost), new PropertyMetadata(null, OnPlayerChanged));

    public Player ReplicaPlayer
    {
        get => (Player)GetValue(ReplicaPlayerProperty);
        set => SetValue(ReplicaPlayerProperty, value);
    }
    public static readonly DependencyProperty ReplicaPlayerProperty =
        DependencyProperty.Register(nameof(ReplicaPlayer), typeof(Player), typeof(FlyleafHost), new PropertyMetadata(null, OnReplicaPlayerChanged));

    public ControlTemplate OverlayTemplate
    {
        get => (ControlTemplate)GetValue(OverlayTemplateProperty);
        set => SetValue(OverlayTemplateProperty, value);
    }
    public static readonly DependencyProperty OverlayTemplateProperty =
        DependencyProperty.Register(nameof(OverlayTemplate), typeof(ControlTemplate), typeof(FlyleafHost), new PropertyMetadata(null, new PropertyChangedCallback(OnOverlayTemplateChanged)));

    public Window Overlay
    {
        get => (Window)GetValue(OverlayProperty);
        set => SetValue(OverlayProperty, value);
    }
    public static readonly DependencyProperty OverlayProperty =
        DependencyProperty.Register(nameof(Overlay), typeof(Window), typeof(FlyleafHost), new PropertyMetadata(null, new PropertyChangedCallback(OnOverlayChanged)));

    public CornerRadius CornerRadius
    {
        get => (CornerRadius)GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(FlyleafHost), new PropertyMetadata(new CornerRadius(0), new PropertyChangedCallback(OnCornerRadiusChanged)));
    #endregion

    #region Events
    private static void OnMouseBindings(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        host.SetMouseSurface();
        host.SetMouseOverlay();
    }
    private static void OnDetachedTopMostChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Surface != null)
            host.Surface.Topmost = !host.IsAttached && host.DetachedTopMost;
    }
    private static void DropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Surface == null)
            return;

        host.Surface.AllowDrop =
            host.OpenOnDrop == AvailableWindows.Surface || host.OpenOnDrop == AvailableWindows.Both ||
            host.SwapOnDrop == AvailableWindows.Surface || host.SwapOnDrop == AvailableWindows.Both;

        if (host.Overlay == null)
            return;

        host.Overlay.AllowDrop =
            host.OpenOnDrop == AvailableWindows.Overlay || host.OpenOnDrop == AvailableWindows.Both ||
            host.SwapOnDrop == AvailableWindows.Overlay || host.SwapOnDrop == AvailableWindows.Both;
    }
    private void UpdateCurRatio()
    {
        if (!KeepRatioOnResize || IsFullScreen)
            return;

        if (Player != null && Player.Video.AspectRatio.Value > 0)
            curResizeRatio = Player.Video.AspectRatio.Value;
        else if (ReplicaPlayer != null && ReplicaPlayer.Video.AspectRatio.Value > 0)
            curResizeRatio = ReplicaPlayer.Video.AspectRatio.Value;
        else
            curResizeRatio = (float)(16.0/9.0);

        curResizeRatioIfEnabled = curResizeRatio;

        Rect screen;

        if (IsAttached)
        {
            if (Owner == null)
            {
                Height = ActualWidth / curResizeRatio;
                return;
            }

            screen = new(zeroPoint, Owner.RenderSize);
        }
        else
        {
            if (Surface == null)
                return;

            var bounds = System.Windows.Forms.Screen.FromPoint(new System.Drawing.Point((int)Surface.Top, (int)Surface.Left)).Bounds;
            screen = new(bounds.Left / DpiX, bounds.Top / DpiY, bounds.Width / DpiX, bounds.Height / DpiY);
        }

        double WindowWidth;
        double WindowHeight;

        if (curResizeRatio >= 1)
        {
            WindowHeight = PreferredLandscapeWidth / curResizeRatio;

            if (WindowHeight < Surface.MinHeight)
            {
                WindowHeight    = Surface.MinHeight;
                WindowWidth     = WindowHeight * curResizeRatio;
            }
            else if (WindowHeight > Surface.MaxHeight)
            {
                WindowHeight    = Surface.MaxHeight;
                WindowWidth     = Surface.Height * curResizeRatio;
            }
            else if (WindowHeight > screen.Height)
            {
                WindowHeight    = screen.Height;
                WindowWidth     = WindowHeight * curResizeRatio;
            }
            else
                WindowWidth = PreferredLandscapeWidth;
        }
        else
        {
            WindowWidth = PreferredPortraitHeight * curResizeRatio;

            if (WindowWidth < Surface.MinWidth)
            {
                WindowWidth     = Surface.MinWidth;
                WindowHeight    = WindowWidth / curResizeRatio;
            }
            else if (WindowWidth > Surface.MaxWidth)
            {
                WindowWidth     = Surface.MaxWidth;
                WindowHeight    = WindowWidth / curResizeRatio;
            }
            else if (WindowWidth > screen.Width)
            {
                WindowWidth     = screen.Width;
                WindowHeight    = WindowWidth / curResizeRatio;
            }
            else
                WindowHeight    = PreferredPortraitHeight;
        }

        if (IsAttached)
        {

            Height  = WindowHeight;
            Width   = WindowWidth;
        }

        else if (Surface != null)
        {
            double WindowLeft;
            double WindowTop;

            if (Surface.Left + Surface.Width / 2 > screen.Width / 2)
                WindowLeft = Math.Min(Math.Max(Surface.Left + Surface.Width - WindowWidth, 0), screen.Width - WindowWidth);
            else
                WindowLeft = Surface.Left;

            if (Surface.Top + Surface.Height / 2 > screen.Height / 2)
                WindowTop = Math.Min(Math.Max(Surface.Top + Surface.Height - WindowHeight, 0), screen.Height - WindowHeight);
            else
                WindowTop = Surface.Top;

            WindowLeft  *= DpiX;
            WindowTop   *= DpiY;
            WindowWidth *= DpiX;
            WindowHeight*= DpiY;

            SetWindowPos(SurfaceHandle, IntPtr.Zero,
                (int)WindowLeft,
                (int)WindowTop,
                (int)Math.Ceiling(WindowWidth),
                (int)Math.Ceiling(WindowHeight),
                (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));
        }
    }
    private static void OnShowInTaskBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Surface == null)
            return;

        if (host.DetachedShowInTaskbar)
            SetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, GetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE) | (nint)WindowStylesEx.WS_EX_APPWINDOW);
        else
            SetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, GetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE) & ~(nint)WindowStylesEx.WS_EX_APPWINDOW);
    }
    private static void OnNoOwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Surface == null)
            return;

        if (!host.IsAttached)
            host.Surface.Owner = host.DetachedNoOwner ? null : host.Owner;
    }
    private static void OnKeepRatioOnResizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (!host.KeepRatioOnResize)
            host.curResizeRatioIfEnabled = 0;
        else
            host.UpdateCurRatio();
    }
    private static void OnPlayerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        host.SetPlayer((Player)e.OldValue);
    }
    private static void OnReplicaPlayerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        host.SetReplicaPlayer((Player)e.OldValue);
    }
    private static void OnIsFullScreenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        host.RefreshNormalFullScreen();
    }
    private static void OnIsMinimizedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        host.Surface.WindowState = host.IsMinimized ? WindowState.Minimized : (host.IsFullScreen ? WindowState.Maximized : WindowState.Normal);
    }
    private static void OnIsAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.IsStandAlone)
        {
            host.IsAttached = false;
            return;
        }

        if (!host.IsLoaded)
            return;

        if (host.IsAttached)
            host.Attach();
        else
            host.Detach();
    }
    private static void OnActivityTimeoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Player == null)
            return;

        host.Player.Activity.Timeout = host.ActivityTimeout;
    }
    bool setTemplate; // Issue #481 - FlyleafME override SetOverlay will not have a template to initialize properly *bool required if SetOverlay can be called multiple times and with different configs
    private static void OnOverlayTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Overlay == null)
        {
            host.setTemplate= true;
            host.Overlay    = new Window() { WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, AllowsTransparency = true };
            host.setTemplate= false;
        }
        else
            host.Overlay.Template = host.OverlayTemplate;
    }
    private static void OnOverlayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (!isDesignMode)
            host.SetOverlay();
        else
        {
            // XSurface.Wpf.Window (can this work on designer?
        }
    }
    private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isDesignMode)
            return;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return;

        if (host.Surface == null)
            return;

        if (host.CornerRadius == host.zeroCornerRadius)
            host.Surface.Background  = Brushes.Black;
        else
        {
            host.Surface.Background  = Brushes.Transparent;
            host.SetCornerRadiusBorder();
        }

        if (host?.Player == null)
            return;

        host.Player.renderer.CornerRadius = (CornerRadius)e.NewValue;

    }
    private void SetCornerRadiusBorder()
    {
        // Required to handle mouse events as the window's background will be transparent
        // This does not set the background color we do that with the renderer (which causes some issues eg. when returning from fullscreen to normalscreen)
        Surface.Content = new Border()
        {
            Background          = Brushes.Black, // TBR: for alpha channel -> Background == Brushes.Transparent || Background ==null ? new SolidColorBrush(Color.FromArgb(1,0,0,0)) : Background
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment   = VerticalAlignment.Stretch,
            CornerRadius        = CornerRadius,
        };
    }
    private static object OnContentChanging(DependencyObject d, object baseValue)
    {
        if (isDesignMode)
            return baseValue;

        FlyleafHost host = d as FlyleafHost;
        if (host.Disposed)
            return host.DetachedContent;

        if (baseValue != null && host.Overlay == null)
            host.Overlay = new Window() { WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, AllowsTransparency = true };

        if (host.Overlay != null)
            host.Overlay.Content = baseValue;

        return host.DetachedContent;
    }

    private void Host_Loaded(object sender, RoutedEventArgs e)
    {
        Window owner = Window.GetWindow(this);
        if (owner == null)
            return;

        var ownerHandle = new WindowInteropHelper(owner).EnsureHandle();

        // Owner Changed
        if (Owner != null)
        {
            if (!IsAttached || OwnerHandle == ownerHandle)
                return; // Check OwnerHandle changed (NOTE: Owner can be the same class/window but the handle can be different)

            Owner.SizeChanged -= Owner_SizeChanged;

            Surface.Hide();
            Overlay?.Hide();
            Detach();

            Owner           = owner;
            OwnerHandle     = ownerHandle;
            Surface.Title   = Owner.Title;
            Surface.Icon    = Owner.Icon;

            Owner.SizeChanged += Owner_SizeChanged;
            Attach();
            rectDetachedLast = Rect.Empty; // Attach will set it wrong first time
            Host_IsVisibleChanged(null, new());

            return;
        }

        Owner           = owner;
        OwnerHandle     = ownerHandle;
        HostDataContext = DataContext;

        SetSurface();

        Surface.Title   = Owner.Title;
        Surface.Icon    = Owner.Icon;

        Owner.SizeChanged   += Owner_SizeChanged;
        DataContextChanged  += Host_DataContextChanged;
        LayoutUpdated       += Host_LayoutUpdated;
        IsVisibleChanged    += Host_IsVisibleChanged;

        // TBR: We need to ensure that Surface/Overlay will be initial Show once to work properly (issue #415)
        if (IsAttached)
        {
            Attach();
            rectDetachedLast = Rect.Empty; // Attach will set it wrong first time
            Surface.Show();
            Overlay?.Show();
            Host_IsVisibleChanged(null, new());
        }
        else
        {
            Detach();

            if (PreferredLandscapeWidth == 0)
                PreferredLandscapeWidth = (int)Surface.Width;

            if (PreferredPortraitHeight == 0)
                PreferredPortraitHeight = (int)Surface.Height;

            UpdateCurRatio();
            Surface.Show();
            Overlay?.Show();
        }
    }

    // WindowChrome Issue #410: It will not properly move child windows when resized from top or left
    private void Owner_SizeChanged(object sender, SizeChangedEventArgs e)
        => rectInitLast = Rect.Empty;

    private void Host_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) =>
        // TBR
        // 1. this.DataContext: FlyleafHost's DataContext will not be affected (Inheritance)
        // 2. Overlay.DataContext: Overlay's DataContext will be FlyleafHost itself
        // 3. Overlay.DataContext.HostDataContext: FlyleafHost's DataContext includes HostDataContext to access FlyleafHost's DataContext
        // 4. In case of Stand Alone will let the user to decide

        HostDataContext = DataContext;
    private void Host_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (!IsAttached)
            return;

        if (IsVisible)
        {
            Host_Loaded(null, null);
            Surface.Show();

            if (Overlay != null)
            {
                Overlay.Show();

                // It happens (eg. with MetroWindow) that overlay left will not be equal to surface left so we reset it by detach/attach the overlay to surface (https://github.com/SuRGeoNix/Flyleaf/issues/370)
                RECT surfRect = new();
                RECT overRect = new();
                GetWindowRect(SurfaceHandle, ref surfRect);
                GetWindowRect(OverlayHandle, ref overRect);

                if (surfRect.Left != overRect.Left)
                {
                    // Detach Overlay
                    SetParent(OverlayHandle, IntPtr.Zero);
                    SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE);
                    Overlay.Owner = null;

                    SetWindowPos(OverlayHandle, IntPtr.Zero, 0, 0, (int)Surface.ActualWidth, (int)Surface.ActualHeight,
                        (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));

                    // Attache Overlay
                    SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)(WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE));
                    Overlay.Owner = Surface;
                    SetParent(OverlayHandle, SurfaceHandle);

                    // Required to restore overlay
                    Rect tt1 = new(0, 0, 0, 0);
                    SetRect(ref tt1);
                }
            }

            // TBR: First time loaded in a tab control could cause UCEERR_RENDERTHREADFAILURE (can be avoided by hide/show again here)
        }
        else
        {
            Surface.Hide();
            Overlay?.Hide();
        }
    }
    private void Host_LayoutUpdated(object sender, EventArgs e)
    {
        // Finds Rect Intersect with FlyleafHost's parents and Clips Surface/Overlay (eg. within ScrollViewer)
        // TBR: Option not to clip rect or stop at first/second parent?
        // For performance should focus only on ScrollViewer if any and Owner Window (other sources that clip our host?)

        if (!IsVisible || !IsAttached || IsFullScreen || IsResizing)
            return;

        try
        {
            rectInit = rectIntersect = new(TransformToAncestor(Owner).Transform(zeroPoint), RenderSize);

            FrameworkElement parent = this;
            while ((parent = VisualTreeHelper.GetParent(parent) as FrameworkElement) != null)
            {
                if (parent.FlowDirection == FlowDirection.RightToLeft)
                {
                    var location = parent.TransformToAncestor(Owner).Transform(zeroPoint);
                    location.X -= parent.RenderSize.Width;
                    rectIntersect.Intersect(new Rect(location, parent.RenderSize));
                }
                else
                    rectIntersect.Intersect(new Rect(parent.TransformToAncestor(Owner).Transform(zeroPoint), parent.RenderSize));
            }

            if (rectInit != rectInitLast)
            {
                SetRect(ref rectInit);
                rectInitLast = rectInit;
            }

            if (rectIntersect == Rect.Empty)
            {
                if (rectIntersect == rectIntersectLast)
                    return;

                rectIntersectLast = rectIntersect;
                SetVisibleRect(ref zeroRect);
            }
            else
            {
                rectIntersect.X -= rectInit.X;
                rectIntersect.Y -= rectInit.Y;

                if (rectIntersect == rectIntersectLast)
                    return;

                rectIntersectLast = rectIntersect;

                SetVisibleRect(ref rectIntersect);
            }
        }
        catch (Exception ex)
        {
            // It has been noticed with NavigationService (The visual tree changes, visual root IsVisible is false but FlyleafHost is still visible)
            if (Logger.CanDebug) Log.Debug($"Host_LayoutUpdated: {ex.Message}");

            // TBR: (Currently handle on each time Visible=true) It's possible that the owner/parent has been changed (for some reason Host_Loaded will not be called) *probably when the Owner stays the same but the actual Handle changes
            //if (ex.Message == "The specified Visual is not an ancestor of this Visual.")
                //Host_Loaded(null, null);
        }
    }
    private void Player_Video_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (KeepRatioOnResize && e.PropertyName == nameof(Player.Video.AspectRatio) && Player.Video.AspectRatio.Value > 0)
            UpdateCurRatio();
    }
    private void ReplicaPlayer_Video_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (KeepRatioOnResize && e.PropertyName == nameof(ReplicaPlayer.Video.AspectRatio) && ReplicaPlayer.Video.AspectRatio.Value > 0)
            UpdateCurRatio();
    }
    #endregion

    #region Events Surface / Overlay
    private void Surface_KeyDown(object sender, KeyEventArgs e) { if (KeyBindings == AvailableWindows.Surface || KeyBindings == AvailableWindows.Both) e.Handled = Player.KeyDown(Player, e); }
    private void Overlay_KeyDown(object sender, KeyEventArgs e) { if (KeyBindings == AvailableWindows.Overlay || KeyBindings == AvailableWindows.Both) e.Handled = Player.KeyDown(Player, e); }

    private void Surface_KeyUp(object sender, KeyEventArgs e) { if (KeyBindings == AvailableWindows.Surface || KeyBindings == AvailableWindows.Both) e.Handled = Player.KeyUp(Player, e); }
    private void Overlay_KeyUp(object sender, KeyEventArgs e) { if (KeyBindings == AvailableWindows.Overlay || KeyBindings == AvailableWindows.Both) e.Handled = Player.KeyUp(Player, e); }

    private void Surface_Drop(object sender, DragEventArgs e)
    {
        IsSwappingStarted = false;
        Surface.ReleaseMouseCapture();
        FlyleafHostDropWrap hostWrap = (FlyleafHostDropWrap) e.Data.GetData(typeof(FlyleafHostDropWrap));

        // Swap FlyleafHosts
        if (hostWrap != null)
        {
            (hostWrap.FlyleafHost.Player, Player) = (Player, hostWrap.FlyleafHost.Player);
            Surface.Activate();
            return;
        }

        if (Player == null)
            return;

        // Invoke event first and see if it gets handled
        OnSurfaceDrop?.Invoke(this, e);

        if (!e.Handled)
        {
            // Player Open Text (TBR: Priority matters, eg. firefox will set both - cached file thumbnail of a video & the link of the video)
            if (e.Data.GetDataPresent(DataFormats.Text))
            {
                string text = e.Data.GetData(DataFormats.Text, false).ToString();
                if (text.Length > 0)
                    Player.OpenAsync(text);
            }

            // Player Open File
            else if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string filename = ((string[])e.Data.GetData(DataFormats.FileDrop, false))[0];
                Player.OpenAsync(filename);
            }
        }

        Surface.Activate();
    }
    private void Overlay_Drop(object sender, DragEventArgs e)
    {
        IsSwappingStarted = false;
        Overlay.ReleaseMouseCapture();
        FlyleafHostDropWrap hostWrap = (FlyleafHostDropWrap) e.Data.GetData(typeof(FlyleafHostDropWrap));

        // Swap FlyleafHosts
        if (hostWrap != null)
        {
            (hostWrap.FlyleafHost.Player, Player) = (Player, hostWrap.FlyleafHost.Player);
            Overlay.Activate();
            return;
        }

        if (Player == null)
            return;

        // Invoke event first and see if it gets handled
        OnOverlayDrop?.Invoke(this, e);

        if (!e.Handled)
        {
            // Player Open Text
            if (e.Data.GetDataPresent(DataFormats.Text))
            {
                string text = e.Data.GetData(DataFormats.Text, false).ToString();
                if (text.Length > 0)
                    Player.OpenAsync(text);
            }

            // Player Open File
            else if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string filename = ((string[])e.Data.GetData(DataFormats.FileDrop, false))[0];
                Player.OpenAsync(filename);
            }
        }

        Overlay.Activate();
    }
    private void Surface_DragEnter(object sender, DragEventArgs e) { if (Player != null) e.Effects = DragDropEffects.All; }
    private void Overlay_DragEnter(object sender, DragEventArgs e) { if (Player != null) e.Effects = DragDropEffects.All; }
    private void Surface_StateChanged(object sender, EventArgs e)
    {
        switch (Surface.WindowState)
        {
            case WindowState.Maximized:
                IsFullScreen = true;
                IsMinimized = false;
                Player?.Activity.RefreshFullActive();

                break;

            case WindowState.Normal:

                IsFullScreen = false;
                IsMinimized = false;
                Player?.Activity.RefreshFullActive();

                break;

            case WindowState.Minimized:

                IsMinimized = true;
                break;
        }
    }

    private void Surface_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => SO_MouseLeftButtonDown(e, Surface);
    private void Overlay_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => SO_MouseLeftButtonDown(e, Overlay);
    private void SO_MouseLeftButtonDown(MouseButtonEventArgs e, Window window)
    {
        AvailableWindows availWindow;
        AttachedDragMoveOptions availDragMove;
        AttachedDragMoveOptions availDragMoveOwner;

        if (window == Surface)
        {
            availWindow         = AvailableWindows.Surface;
            availDragMove       = AttachedDragMoveOptions.Surface;
            availDragMoveOwner  = AttachedDragMoveOptions.SurfaceOwner;
        }
        else
        {
            availWindow         = AvailableWindows.Overlay;
            availDragMove       = AttachedDragMoveOptions.Overlay;
            availDragMoveOwner  = AttachedDragMoveOptions.OverlayOwner;
        }

        if (BringToFrontOnClick) // Activate and Z-order top
            BringToFront();

        window.Focus();
        Player?.Activity.RefreshFullActive();

        mouseLeftDownPoint = e.GetPosition(window);
        IsSwappingStarted = false; // currently we don't care if it was cancelled (it can be stay true if we miss the mouse up) - QueryContinueDrag

        // Resize
        if (ResizingSide != 0)
        {
            IsResizing = true;

            if (IsAttached)
            {
                ownerZeroPointPos = Owner.PointToScreen(zeroPoint);
                GetWindowRect(SurfaceHandle, ref beforeResizeRect);
                LayoutUpdated -= Host_LayoutUpdated;
                ResetVisibleRect();
            }
        }

        // Swap
        else if ((SwapOnDrop == availWindow || SwapOnDrop == AvailableWindows.Both) &&
            (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)))
        {
            IsSwappingStarted = true;
            DragDrop.DoDragDrop(this, new FlyleafHostDropWrap() { FlyleafHost = this }, DragDropEffects.Move);

            return; // No Capture
        }

        // PanMove
        else if (Player != null &&
            (PanMoveOnCtrl == availWindow || PanMoveOnCtrl == AvailableWindows.Both) &&
            (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)))
        {
            panPrevX    = Player.PanXOffset;
            panPrevY    = Player.PanYOffset;
            IsPanMoving = true;
        }

        // DragMoveOwner
        else if (IsAttached && Owner != null &&
            (AttachedDragMove == availDragMoveOwner || AttachedDragMove == AttachedDragMoveOptions.BothOwner))
            IsDragMovingOwner = true;

        // DragMove (Attach|Detach)
        else if ((IsAttached && (AttachedDragMove == availDragMove  || AttachedDragMove == AttachedDragMoveOptions.Both))
            ||  (!IsAttached && (DetachedDragMove == availWindow    || DetachedDragMove == AvailableWindows.Both)))
            IsDragMoving = true;

        else
            return; // No Capture

        window.CaptureMouse();
    }

    private void Surface_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => Surface_ReleaseCapture();
    private void Overlay_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => Overlay_ReleaseCapture();
    private void Surface_LostMouseCapture(object sender, MouseEventArgs e) => Surface_ReleaseCapture();
    private void Overlay_LostMouseCapture(object sender, MouseEventArgs e) => Overlay_ReleaseCapture();
    private void Surface_ReleaseCapture()
    {
        if (!IsResizing && !IsPanMoving && !IsDragMoving && !IsDragMovingOwner)
            return;

        Surface.ReleaseMouseCapture();

        if (IsResizing)
        {
            ResizingSide    = 0;
            Surface.Cursor  = Cursors.Arrow;
            IsResizing      = false;

            if (IsAttached)
            {
                GetWindowRect(SurfaceHandle, ref curRect);
                MarginTarget.Margin = new(MarginTarget.Margin.Left + (curRect.Left - beforeResizeRect.Left) / DpiX, MarginTarget.Margin.Top + (curRect.Top - beforeResizeRect.Top) / DpiY, MarginTarget.Margin.Right, MarginTarget.Margin.Bottom);
                Width   = Surface.Width;
                Height  = Surface.Height;
                Host_LayoutUpdated(null, null); // When attached to restore the clipped rect
                LayoutUpdated += Host_LayoutUpdated;
            }
        }
        else if (IsPanMoving)
            IsPanMoving = false;
        else if (IsDragMoving)
            IsDragMoving = false;
        else if (IsDragMovingOwner)
            IsDragMovingOwner = false;
        else
            return;
    }
    private void Overlay_ReleaseCapture()
    {
        if (!IsResizing && !IsPanMoving && !IsDragMoving && !IsDragMovingOwner)
            return;

        Overlay.ReleaseMouseCapture();

        if (IsResizing)
        {
            ResizingSide    = 0;
            Overlay.Cursor  = Cursors.Arrow;
            IsResizing      = false;

            if (IsAttached)
            {
                GetWindowRect(SurfaceHandle, ref curRect);
                MarginTarget.Margin = new(MarginTarget.Margin.Left + (curRect.Left - beforeResizeRect.Left) / DpiX, MarginTarget.Margin.Top + (curRect.Top - beforeResizeRect.Top) / DpiY, MarginTarget.Margin.Right, MarginTarget.Margin.Bottom);
                Width   = Surface.Width;
                Height  = Surface.Height;
                Host_LayoutUpdated(null, null); // When attached to restore the clipped rect
                LayoutUpdated += Host_LayoutUpdated;
            }
        }
        else if (IsPanMoving)
            IsPanMoving = false;
        else if (IsDragMoving)
            IsDragMoving = false;
        else if (IsDragMovingOwner)
            IsDragMovingOwner = false;
    }

    private void Surface_MouseMove(object sender, MouseEventArgs e)
    {
        var cur = e.GetPosition(Surface);

        if (Player != null && cur != mouseMoveLastPoint)
        {
            Player.Activity.RefreshFullActive();
            mouseMoveLastPoint = cur;
        }

        // Resize Sides (CanResize + !MouseDown + !FullScreen)
        if (e.MouseDevice.LeftButton != MouseButtonState.Pressed)
        {
            if ( !IsFullScreen &&
                ((IsAttached && (AttachedResize == AvailableWindows.Surface || AttachedResize == AvailableWindows.Both)) ||
                (!IsAttached && (DetachedResize == AvailableWindows.Surface || DetachedResize == AvailableWindows.Both))))
            {
                ResizingSide = ResizeSides(Surface, cur, ResizeSensitivity, CornerRadius);
            }

            return;
        }

        SO_MouseLeftDownAndMove(cur);
    }
    private void Overlay_MouseMove(object sender, MouseEventArgs e)
    {
        var cur = e.GetPosition(Overlay);

        if (Player != null && cur != mouseMoveLastPoint)
        {
            Player.Activity.RefreshFullActive();
            mouseMoveLastPoint = cur;
        }

        // Resize Sides (CanResize + !MouseDown + !FullScreen)
        if (e.MouseDevice.LeftButton != MouseButtonState.Pressed)
        {
            if (!IsFullScreen && cur != zeroPoint &&
                ((IsAttached && (AttachedResize == AvailableWindows.Overlay || AttachedResize == AvailableWindows.Both)) ||
                (!IsAttached && (DetachedResize == AvailableWindows.Overlay || DetachedResize == AvailableWindows.Both))))
            {
                ResizingSide = ResizeSides(Overlay, cur, ResizeSensitivity, CornerRadius);
            }

            return;
        }

        SO_MouseLeftDownAndMove(cur);
    }
    private void SO_MouseLeftDownAndMove(Point cur)
    {
        if (IsSwappingStarted)
            return;

        // Player's Pan Move (Ctrl + Drag Move)
        if (IsPanMoving)
        {
            Player.PanXOffset = panPrevX + (int) (cur.X - mouseLeftDownPoint.X);
            Player.PanYOffset = panPrevY + (int) (cur.Y - mouseLeftDownPoint.Y);

            return;
        }

        if (IsFullScreen)
            return;

        // Resize (MouseDown + ResizeSide != 0)
        if (IsResizing)
            Resize(cur, ResizingSide, curResizeRatioIfEnabled);

        // Drag Move Self (Attached|Detached)
        else if (IsDragMoving)
        {
            if (IsAttached)
            {
                MarginTarget.Margin = new(
                    MarginTarget.Margin.Left + cur.X - mouseLeftDownPoint.X,
                    MarginTarget.Margin.Top  + cur.Y - mouseLeftDownPoint.Y,
                    MarginTarget.Margin.Right,
                    MarginTarget.Margin.Bottom);
            }
            else
            {
                Surface.Left  += cur.X - mouseLeftDownPoint.X;
                Surface.Top   += cur.Y - mouseLeftDownPoint.Y;
            }
        }

        // Drag Move Owner (Attached)
        else if (IsDragMovingOwner)
        {
            if (Owner.Owner != null)
            {
                Owner.Owner.Left  += cur.X - mouseLeftDownPoint.X;
                Owner.Owner.Top   += cur.Y - mouseLeftDownPoint.Y;
            }
            else
            {
                Owner.Left  += cur.X - mouseLeftDownPoint.X;
                Owner.Top   += cur.Y - mouseLeftDownPoint.Y;
            }
        }
    }

    private void Surface_MouseLeave(object sender, MouseEventArgs e) { ResizingSide = 0; Surface.Cursor = Cursors.Arrow; }
    private void Overlay_MouseLeave(object sender, MouseEventArgs e) { ResizingSide = 0; Overlay.Cursor = Cursors.Arrow; }

    private void Surface_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (ToggleFullScreenOnDoubleClick == AvailableWindows.Surface || ToggleFullScreenOnDoubleClick == AvailableWindows.Both) { IsFullScreen = !IsFullScreen; e.Handled = true; } }
    private void Overlay_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (ToggleFullScreenOnDoubleClick == AvailableWindows.Overlay || ToggleFullScreenOnDoubleClick == AvailableWindows.Both) { IsFullScreen = !IsFullScreen; e.Handled = true; } }

    private void Surface_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (Player == null || e.Delta == 0)
            return;

        if      ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) &&
            (PanZoomOnCtrlWheel == AvailableWindows.Surface || PanZoomOnCtrlWheel == AvailableWindows.Both))
        {
            var cur = e.GetPosition(Surface);
            Point curDpi = new(cur.X * DpiX, cur.Y * DpiY);
            if (e.Delta > 0)
                Player.ZoomIn(curDpi);
            else
                Player.ZoomOut(curDpi);
        }
        else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) &&
            (PanRotateOnShiftWheel == AvailableWindows.Surface || PanZoomOnCtrlWheel == AvailableWindows.Both))
        {
            if (e.Delta > 0)
                Player.RotateRight();
            else
                Player.RotateLeft();
        }

        //else if (IsAttached) // TBR ScrollViewer
        //{
        //    RaiseEvent(e);
        //}
    }
    private void Overlay_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (Player == null || e.Delta == 0)
            return;

        if      ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) &&
            (PanZoomOnCtrlWheel == AvailableWindows.Overlay || PanZoomOnCtrlWheel == AvailableWindows.Both))
        {
            var cur = e.GetPosition(Overlay);
            Point curDpi = new(cur.X * DpiX, cur.Y * DpiY);
            if (e.Delta > 0)
                Player.ZoomIn(curDpi);
            else
                Player.ZoomOut(curDpi);
        }
        else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) &&
            (PanRotateOnShiftWheel == AvailableWindows.Overlay || PanZoomOnCtrlWheel == AvailableWindows.Both))
        {
            if (e.Delta > 0)
                Player.RotateRight();
            else
                Player.RotateLeft();
        }
    }

    private void Surface_Closed(object sender, EventArgs e)
    {
        surfaceClosed = true;
        Dispose();
    }
    private void Surface_Closing(object sender, CancelEventArgs e) => surfaceClosing = true;
    private void Overlay_Closed(object sender, EventArgs e)
    {
        overlayClosed = true;
        if (!surfaceClosing)
            Surface?.Close();
    }
    private void OverlayStandAlone_Loaded(object sender, RoutedEventArgs e)
    {
        if (Overlay != null)
            return;

        if (standAloneOverlay.WindowStyle != WindowStyle.None || standAloneOverlay.AllowsTransparency == false)
            throw new Exception("Stand-alone FlyleafHost requires WindowStyle = WindowStyle.None and AllowsTransparency = true");

        SetSurface();
        Overlay = standAloneOverlay;
        Overlay.IsVisibleChanged += OverlayStandAlone_IsVisibleChanged;
        Surface.ShowInTaskbar = false; Surface.ShowInTaskbar = true; // It will not be visible in taskbar if user clicks in another window when loading
        OverlayStandAlone_IsVisibleChanged(null, new());
    }
    private void OverlayStandAlone_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        // Surface should be visible first (this happens only on initialization of standalone)

        if (Surface.IsVisible)
            return;

        if (Overlay.IsVisible)
        {
            Surface.Show();
            ShowWindow(OverlayHandle, (int)ShowWindowCommands.SW_SHOWMINIMIZED);
            ShowWindow(OverlayHandle, (int)ShowWindowCommands.SW_SHOWMAXIMIZED);
        }
    }

    public void Resize(Point p, int resizingSide, double ratio = 0.0)
    {
        double WindowWidth  = Surface.ActualWidth;
        double WindowHeight = Surface.ActualHeight;
        double WindowLeft;
        double WindowTop;

        if (IsAttached) // NOTE: Window.Left will not be updated when the Owner moves, don't use it when attached
        {
            GetWindowRect(SurfaceHandle, ref curRect);
            WindowLeft  = (curRect.Left - ownerZeroPointPos.X) / DpiX;
            WindowTop   = (curRect.Top - ownerZeroPointPos.Y) / DpiY;
        }
        else
        {
            WindowLeft  = Surface.Left;
            WindowTop   = Surface.Top;
        }

        if (resizingSide == 2 || resizingSide == 3 || resizingSide == 6)
        {
            p.X += 5;

            WindowWidth = p.X > Surface.MinWidth ?
                p.X < Surface.MaxWidth ? p.X : Surface.MaxWidth :
                Surface.MinWidth;
        }
        else if (resizingSide == 1 || resizingSide == 4 || resizingSide == 5)
        {
            p.X -= 5;
            double temp = Surface.ActualWidth - p.X;
            if (temp > Surface.MinWidth && temp < Surface.MaxWidth)
            {
                WindowWidth = temp;
                WindowLeft  = WindowLeft + p.X;
            }
        }

        if (resizingSide == 2 || resizingSide == 4 || resizingSide == 8)
        {
            p.Y += 5;

            if (p.Y > Surface.MinHeight)
            {
                WindowHeight = p.Y < Surface.MaxHeight ? p.Y : Surface.MaxHeight;
            }
            else
                return;
        }
        else if (resizingSide == 1 || resizingSide == 3 || resizingSide == 7)
        {
            if (ratio != 0 && resizingSide != 7)
            {
                double temp = WindowWidth / ratio;
                if (temp > Surface.MinHeight && temp < Surface.MaxHeight)
                    WindowTop += Surface.ActualHeight - temp;
                else
                    return;
            }
            else
            {
                p.Y -= 5;
                double temp = Surface.ActualHeight - p.Y;
                if (temp > Surface.MinHeight && temp < Surface.MaxHeight)
                {
                    WindowHeight= temp;
                    WindowTop += p.Y;
                }
                else
                    return;
            }
        }

        if (ratio != 0)
        {
            if (resizingSide == 7 || resizingSide == 8)
                WindowWidth = WindowHeight * ratio;
            else
                WindowHeight = WindowWidth / ratio;
        }

        if (WindowWidth >= WindowHeight)
            PreferredLandscapeWidth = (int)WindowWidth;
        else
            PreferredPortraitHeight = (int)WindowHeight;

        WindowLeft  *= DpiX;
        WindowTop   *= DpiY;
        WindowWidth *= DpiX;
        WindowHeight*= DpiY;

        SetWindowPos(SurfaceHandle, IntPtr.Zero,
            (int)WindowLeft,
            (int)WindowTop,
            (int)Math.Ceiling(WindowWidth),
            (int)Math.Ceiling(WindowHeight),
            (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));
    }
    public static int ResizeSides(Window Window, Point p, int ResizeSensitivity, CornerRadius cornerRadius)
    {
        if (p.X <= ResizeSensitivity + (cornerRadius.TopLeft / 2) && p.Y <= ResizeSensitivity + (cornerRadius.TopLeft / 2))
        {
            Window.Cursor = Cursors.SizeNWSE;
            return 1;
        }
        else if (p.X + ResizeSensitivity + (cornerRadius.BottomRight / 2) >= Window.ActualWidth && p.Y + ResizeSensitivity + (cornerRadius.BottomRight / 2) >= Window.ActualHeight)
        {
            Window.Cursor = Cursors.SizeNWSE;
            return 2;
        }
        else if (p.X + ResizeSensitivity + (cornerRadius.TopRight / 2) >= Window.ActualWidth && p.Y <= ResizeSensitivity + (cornerRadius.TopRight / 2))
        {
            Window.Cursor = Cursors.SizeNESW;
            return 3;
        }
        else if (p.X <= ResizeSensitivity + (cornerRadius.BottomLeft / 2) && p.Y + ResizeSensitivity + (cornerRadius.BottomLeft / 2)  >= Window.ActualHeight)
        {
            Window.Cursor = Cursors.SizeNESW;
            return 4;
        }
        else if (p.X <= ResizeSensitivity)
        {
            Window.Cursor = Cursors.SizeWE;
            return 5;
        }
        else if (p.X + ResizeSensitivity >= Window.ActualWidth)
        {
            Window.Cursor = Cursors.SizeWE;
            return 6;
        }
        else if (p.Y <= ResizeSensitivity)
        {
            Window.Cursor = Cursors.SizeNS;
            return 7;
        }
        else if (p.Y + ResizeSensitivity >= Window.ActualHeight)
        {
            Window.Cursor = Cursors.SizeNS;
            return 8;
        }
        else
        {
            Window.Cursor = Cursors.Arrow;
            return 0;
        }
    }
    #endregion

    #region Constructors
    static FlyleafHost()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(FlyleafHost), new FrameworkPropertyMetadata(typeof(FlyleafHost)));
        ContentProperty.OverrideMetadata(typeof(FlyleafHost), new FrameworkPropertyMetadata(null, new CoerceValueCallback(OnContentChanging)));
    }
    public FlyleafHost()
    {
        UniqueId = idGenerator++;
        isDesignMode = DesignerProperties.GetIsInDesignMode(this);
        if (isDesignMode)
            return;

        MarginTarget= this;
        Log         = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + $" [FlyleafHost NP] ");
        Loaded     += Host_Loaded;
    }
    public FlyleafHost(Window standAloneOverlay)
    {
        UniqueId = idGenerator++;
        Log = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + $" [FlyleafHost NP] ");

        IsStandAlone = true;
        IsAttached = false;

        this.standAloneOverlay = standAloneOverlay;
        standAloneOverlay.Loaded += OverlayStandAlone_Loaded;
        if (standAloneOverlay.IsLoaded)
            OverlayStandAlone_Loaded(null, null);
    }
    #endregion

    #region Methods
    public virtual void SetReplicaPlayer(Player oldPlayer)
    {
        if (oldPlayer != null)
        {
            oldPlayer.renderer.SetChildHandle(IntPtr.Zero);
            oldPlayer.Video.PropertyChanged -= ReplicaPlayer_Video_PropertyChanged;
        }

        if (ReplicaPlayer == null)
            return;

        if (Surface != null)
            ReplicaPlayer.renderer.SetChildHandle(SurfaceHandle);

        ReplicaPlayer.Video.PropertyChanged += ReplicaPlayer_Video_PropertyChanged;
    }
    public virtual void SetPlayer(Player oldPlayer)
    {
        // De-assign old Player's Handle/FlyleafHost
        if (oldPlayer != null)
        {
            Log.Debug($"De-assign Player #{oldPlayer.PlayerId}");

            oldPlayer.Video.PropertyChanged -= Player_Video_PropertyChanged;
            oldPlayer.VideoDecoder.DestroySwapChain();
            oldPlayer.Host = null;
        }

        if (Player == null)
            return;

        Log.Prefix = ("[#" + UniqueId + "]").PadRight(8, ' ') + $" [FlyleafHost #{Player.PlayerId}] ";

        // De-assign new Player's Handle/FlyleafHost
        Player.Host?.Player_Disposed();

        if (Player == null) // We might just de-assign our Player
            return;

        // Assign new Player's (Handle/FlyleafHost)
        Log.Debug($"Assign Player #{Player.PlayerId}");

        Player.Host = this;
        Player.Activity.Timeout = ActivityTimeout;
        if (Player.renderer != null) // TBR: using as AudioOnly with a Control*
            Player.renderer.CornerRadius = IsFullScreen ? zeroCornerRadius : CornerRadius;

        if (Surface != null)
        {
            if (CornerRadius == zeroCornerRadius)
                Surface.Background = new SolidColorBrush(Player.Config.Video.BackgroundColor);
            //else // TBR: this border probably not required? only when we don't have a renderer?
                //((Border)Surface.Content).Background = new SolidColorBrush(Player.Config.Video.BackgroundColor);

            Player.VideoDecoder.CreateSwapChain(SurfaceHandle);
        }

        Player.Video.PropertyChanged += Player_Video_PropertyChanged;
        UpdateCurRatio();
    }
    public virtual void SetSurface(bool fromSetOverlay = false)
    {
        if (Surface != null)
            return;

        // Required for some reason (WindowStyle.None will not be updated with our style)
        Surface             = new();
        Surface.Name        = $"Surface_{UniqueId}";
        Surface.Width       = Surface.Height = 1; // Will be set on loaded
        Surface.WindowStyle = WindowStyle.None;
        Surface.ResizeMode  = ResizeMode.NoResize;
        Surface.ShowInTaskbar = false;

        // CornerRadius must be set initially to AllowsTransparency!
        if (CornerRadius == zeroCornerRadius)
            Surface.Background = Player != null ? new SolidColorBrush(Player.Config.Video.BackgroundColor) : Brushes.Black;
        else
        {
            Surface.AllowsTransparency  = true;
            Surface.Background          = Brushes.Transparent;
            SetCornerRadiusBorder();
        }

        // When using ItemsControl with ObservableCollection<Player> to fill DataTemplates with FlyleafHost EnsureHandle will call Host_loaded
        if (IsAttached) Loaded -= Host_Loaded;
        SurfaceHandle = new WindowInteropHelper(Surface).EnsureHandle();
        if (IsAttached) Loaded += Host_Loaded;

        if (IsAttached)
        {
            SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)WindowStyles.WS_CHILD);
            SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)WindowStylesEx.WS_EX_LAYERED);
        }
        else // Detached || StandAlone
        {
            SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE);
            if (DetachedShowInTaskbar || IsStandAlone)
                SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)(WindowStylesEx.WS_EX_APPWINDOW | WindowStylesEx.WS_EX_LAYERED));
            else
                SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)WindowStylesEx.WS_EX_LAYERED);
        }

        if (Player != null)
            Player.VideoDecoder.CreateSwapChain(SurfaceHandle);

        if (ReplicaPlayer != null)
            ReplicaPlayer.renderer.SetChildHandle(SurfaceHandle);

        Surface.Closed      += Surface_Closed;
        Surface.Closing     += Surface_Closing;
        Surface.KeyDown     += Surface_KeyDown;
        Surface.KeyUp       += Surface_KeyUp;
        Surface.Drop        += Surface_Drop;
        Surface.DragEnter   += Surface_DragEnter;
        Surface.StateChanged+= Surface_StateChanged;
        Surface.SizeChanged += SetRectOverlay;

        SetMouseSurface();

        Surface.AllowDrop =
            OpenOnDrop == AvailableWindows.Surface || OpenOnDrop == AvailableWindows.Both ||
            SwapOnDrop == AvailableWindows.Surface || SwapOnDrop == AvailableWindows.Both;

        if (IsAttached && IsLoaded && Owner == null && !fromSetOverlay)
            Host_Loaded(null, null);

        SurfaceCreated?.Invoke(this, new());
    }
    public virtual void SetOverlay()
    {
        if (Overlay == null)
            return;

        SetSurface(true);

        if (IsAttached) Loaded -= Host_Loaded;
        OverlayHandle = new WindowInteropHelper(Overlay).EnsureHandle();
        if (IsAttached) Loaded += Host_Loaded;

        if (IsStandAlone)
        {
            if (PreferredLandscapeWidth == 0)
                PreferredLandscapeWidth = (int)Overlay.Width;

            if (PreferredPortraitHeight == 0)
                PreferredPortraitHeight = (int)Overlay.Height;

            SetWindowPos(SurfaceHandle, IntPtr.Zero, (int)Math.Round(Overlay.Left * DpiX), (int)Math.Round(Overlay.Top * DpiY), (int)Math.Round(Overlay.ActualWidth * DpiX), (int)Math.Round(Overlay.ActualHeight * DpiY),
                (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));

            Surface.Title       = Overlay.Title;
            Surface.Icon        = Overlay.Icon;
            Surface.MinHeight   = Overlay.MinHeight;
            Surface.MaxHeight   = Overlay.MaxHeight;
            Surface.MinWidth    = Overlay.MinWidth;
            Surface.MaxWidth    = Overlay.MaxWidth;
            Surface.Topmost     = DetachedTopMost;
        }
        else
        {
            Overlay.Resources   = Resources;
            Overlay.DataContext = this; // TBR: or this.DataContext?
        }

        SetWindowPos(OverlayHandle, IntPtr.Zero, 0, 0, (int)Surface.ActualWidth, (int)Surface.ActualHeight,
                (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));

        Overlay.Name            = $"Overlay_{UniqueId}";
        Overlay.Background      = Brushes.Transparent;
        Overlay.ShowInTaskbar   = false;
        Overlay.Owner           = Surface;
        SetParent(OverlayHandle, SurfaceHandle);
        SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)(WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE)); // TBR: WS_MAXIMIZE required? (possible better for DWM on fullscreen?)

        Overlay.KeyUp       += Overlay_KeyUp;
        Overlay.KeyDown     += Overlay_KeyDown;
        Overlay.Closed      += Overlay_Closed;
        Overlay.Drop        += Overlay_Drop;
        Overlay.DragEnter   += Overlay_DragEnter;

        SetMouseOverlay();

        // Owner will close the overlay
        Overlay.KeyDown += (o, e) => { if (e.Key == Key.System && e.SystemKey == Key.F4) Surface?.Focus(); };

        Overlay.AllowDrop =
            OpenOnDrop == AvailableWindows.Overlay || OpenOnDrop == AvailableWindows.Both ||
            SwapOnDrop == AvailableWindows.Overlay || SwapOnDrop == AvailableWindows.Both;

        if (setTemplate)
            Overlay.Template = OverlayTemplate;

        if (Surface.IsVisible)
            Overlay.Show();
        else if (!IsStandAlone && !Overlay.IsVisible)
        {
            Overlay.Show();
            Overlay.Hide();
        }

        if (IsAttached && IsLoaded && Owner == null)
            Host_Loaded(null, null);

        OverlayCreated?.Invoke(this, new());
    }
    private void SetMouseSurface()
    {
        if (Surface == null)
            return;

        if ((MouseBindings == AvailableWindows.Surface || MouseBindings == AvailableWindows.Both) && !isMouseBindingsSubscribedSurface)
        {
            Surface.LostMouseCapture    += Surface_LostMouseCapture;
            Surface.MouseLeftButtonDown += Surface_MouseLeftButtonDown;
            Surface.MouseLeftButtonUp   += Surface_MouseLeftButtonUp;
            if (PanZoomOnCtrlWheel != AvailableWindows.None ||
                PanRotateOnShiftWheel != AvailableWindows.None)
            {
                Surface.MouseWheel += Surface_MouseWheel;
            }
            Surface.MouseMove           += Surface_MouseMove;
            Surface.MouseLeave          += Surface_MouseLeave;
            if (ToggleFullScreenOnDoubleClick != AvailableWindows.None)
            {
                Surface.MouseDoubleClick += Surface_MouseDoubleClick;
            }
            isMouseBindingsSubscribedSurface = true;
        }
        else if (isMouseBindingsSubscribedSurface)
        {
            Surface.LostMouseCapture    -= Surface_LostMouseCapture;
            Surface.MouseLeftButtonDown -= Surface_MouseLeftButtonDown;
            Surface.MouseLeftButtonUp   -= Surface_MouseLeftButtonUp;
            if (PanZoomOnCtrlWheel != AvailableWindows.None ||
                PanRotateOnShiftWheel != AvailableWindows.None)
            {
                Surface.MouseWheel -= Surface_MouseWheel;
            }
            Surface.MouseMove           -= Surface_MouseMove;
            Surface.MouseLeave          -= Surface_MouseLeave;
            if (ToggleFullScreenOnDoubleClick != AvailableWindows.None)
            {
                Surface.MouseDoubleClick -= Surface_MouseDoubleClick;
            }
            isMouseBindingsSubscribedSurface = false;
        }
    }
    private void SetMouseOverlay()
    {
        if (Overlay == null)
            return;

        if ((MouseBindings == AvailableWindows.Overlay || MouseBindings == AvailableWindows.Both) && !isMouseBindingsSubscribedOverlay)
        {
            Overlay.LostMouseCapture    += Overlay_LostMouseCapture;
            Overlay.MouseLeftButtonDown += Overlay_MouseLeftButtonDown;
            Overlay.MouseLeftButtonUp   += Overlay_MouseLeftButtonUp;
            if (PanZoomOnCtrlWheel != AvailableWindows.None ||
                PanRotateOnShiftWheel != AvailableWindows.None)
            {
                Overlay.MouseWheel += Overlay_MouseWheel;
            }
            Overlay.MouseMove           += Overlay_MouseMove;
            Overlay.MouseLeave          += Overlay_MouseLeave;
            if (ToggleFullScreenOnDoubleClick != AvailableWindows.None)
            {
                Overlay.MouseDoubleClick += Overlay_MouseDoubleClick;
            }
            isMouseBindingsSubscribedOverlay = true;
        }
        else if (isMouseBindingsSubscribedOverlay)
        {
            Overlay.LostMouseCapture    -= Overlay_LostMouseCapture;
            Overlay.MouseLeftButtonDown -= Overlay_MouseLeftButtonDown;
            Overlay.MouseLeftButtonUp   -= Overlay_MouseLeftButtonUp;
            if (PanZoomOnCtrlWheel != AvailableWindows.None ||
                PanRotateOnShiftWheel != AvailableWindows.None)
            {
                Overlay.MouseWheel -= Overlay_MouseWheel;
            }
            Overlay.MouseMove           -= Overlay_MouseMove;
            Overlay.MouseLeave          -= Overlay_MouseLeave;
            if (ToggleFullScreenOnDoubleClick != AvailableWindows.None)
            {
                Overlay.MouseDoubleClick -= Overlay_MouseDoubleClick;
            }

            isMouseBindingsSubscribedOverlay = false;
        }
    }

    public virtual void Attach(bool ignoreRestoreRect = false)
    {
        Window wasFocus = Overlay != null && Overlay.IsKeyboardFocusWithin ? Overlay : Surface;

        if (IsFullScreen)
        {
            IsFullScreen = false;
            return;
        }
        if (!ignoreRestoreRect)
            rectDetachedLast= new(Surface.Left, Surface.Top, Surface.Width, Surface.Height);

        Surface.Topmost     = false;
        Surface.MinWidth    = MinWidth;
        Surface.MinHeight   = MinHeight;
        Surface.MaxWidth    = MaxWidth;
        Surface.MaxHeight   = MaxHeight;

        SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)WindowStyles.WS_CHILD);
        Surface.Owner = Owner;
        SetParent(SurfaceHandle, OwnerHandle);

        rectInitLast = rectIntersectLast = rectRandom;
        Host_LayoutUpdated(null, null);
        Owner.Activate();
        wasFocus.Focus();
    }
    public virtual void Detach()
    {
        if (IsFullScreen)
            IsFullScreen = false;

        Surface.MinWidth    = DetachedMinWidth;
        Surface.MinHeight   = DetachedMinHeight;
        Surface.MaxWidth    = DetachedMaxWidth;
        Surface.MaxHeight   = DetachedMaxHeight;

        // Calculate Size
        var newSize = DetachedRememberSize && rectDetachedLast != Rect.Empty
            ? new Size(rectDetachedLast.Width, rectDetachedLast.Height)
            : DetachedFixedSize;

        // Calculate Position
        Point newPos;
        if (DetachedRememberPosition && rectDetachedLast != Rect.Empty)
            newPos = new Point(rectDetachedLast.X, rectDetachedLast.Y);
        else
        {
            var screen = System.Windows.Forms.Screen.FromPoint(new System.Drawing.Point((int)Surface.Top, (int)Surface.Left)).Bounds;

            // Drop Dpi to work with screen (no Dpi)
            newSize.Width   *= DpiX;
            newSize.Height  *= DpiY;

            switch (DetachedPosition)
            {
                case DetachedPositionOptions.TopLeft:
                    newPos = new Point(screen.Left, screen.Top);
                    break;
                case DetachedPositionOptions.TopCenter:
                    newPos = new Point(screen.Left + (screen.Width / 2) - (newSize.Width / 2), screen.Top);
                    break;

                case DetachedPositionOptions.TopRight:
                    newPos = new Point(screen.Left + screen.Width - newSize.Width, screen.Top);
                    break;

                case DetachedPositionOptions.CenterLeft:
                    newPos = new Point(screen.Left, screen.Top + (screen.Height / 2) - (newSize.Height / 2));
                    break;

                case DetachedPositionOptions.CenterCenter:
                    newPos = new Point(screen.Left + (screen.Width / 2) - (newSize.Width / 2), screen.Top + (screen.Height / 2) - (newSize.Height / 2));
                    break;

                case DetachedPositionOptions.CenterRight:
                    newPos = new Point(screen.Left + screen.Width - newSize.Width, screen.Top + (screen.Height / 2) - (newSize.Height / 2));
                    break;

                case DetachedPositionOptions.BottomLeft:
                    newPos = new Point(screen.Left, screen.Top + screen.Height - newSize.Height);
                    break;

                case DetachedPositionOptions.BottomCenter:
                    newPos = new Point(screen.Left + (screen.Width / 2) - (newSize.Width / 2), screen.Top + screen.Height - newSize.Height);
                    break;

                case DetachedPositionOptions.BottomRight:
                    newPos = new Point(screen.Left + screen.Width - newSize.Width, screen.Top + screen.Height - newSize.Height);
                    break;

                case DetachedPositionOptions.Custom:
                    newPos = DetachedFixedPosition;
                    break;

                default:
                    newPos = new(); //satisfy the compiler
                    break;
            }

            // SetRect will drop DPI so we add it
            newPos.X /= DpiX;
            newPos.Y /= DpiY;

            newPos.X += DetachedPositionMargin.Left - DetachedPositionMargin.Right;
            newPos.Y += DetachedPositionMargin.Top - DetachedPositionMargin.Bottom;

            // Restore DPI
            newSize.Width   /= DpiX;
            newSize.Height  /= DpiY;
        }

        Rect final = new(newPos.X, newPos.Y, newSize.Width, newSize.Height);

        // Detach (Parent=Null, Owner=Null ?, ShowInTaskBar?, TopMost?)
        SetParent(SurfaceHandle, IntPtr.Zero);
        SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD);
        Surface.Owner = DetachedNoOwner ? null : Owner;
        Surface.Topmost = DetachedTopMost;

        SetRect(ref final);
        ResetVisibleRect();

        if (Surface.IsVisible) // Initially detached will not be visible yet and activate not required (in case of multiple)
            Surface.Activate();
    }

    public void RefreshNormalFullScreen()
    {
        if (IsFullScreen)
        {
            if (IsAttached)
            {
                // When we set the parent to null we don't really know in which left/top will be transfered and maximized into random screen
                GetWindowRect(SurfaceHandle, ref curRect);

                ResetVisibleRect();
                SetParent(SurfaceHandle, IntPtr.Zero);
                SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD);
                Surface.Owner   = DetachedNoOwner ? null : Owner;
                Surface.Topmost = DetachedTopMost;

                SetWindowPos(SurfaceHandle, IntPtr.Zero, curRect.Left, curRect.Top, 0, 0, (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_NOSIZE));
            }

            if (Player != null)
                Player.renderer.CornerRadius = zeroCornerRadius;

            if (CornerRadius != zeroCornerRadius)
                ((Border)Surface.Content).CornerRadius = zeroCornerRadius;

            Surface.WindowState = WindowState.Maximized;

            // If it was above the borders and double click (mouse didn't move to refresh)
            Surface.Cursor = Cursors.Arrow;
            if (Overlay != null)
                Overlay.Cursor = Cursors.Arrow;
        }
        else
        {
            if (IsStandAlone)
                Surface.WindowState = WindowState.Normal;

            if (IsAttached)
            {
                Attach(true);
                InvalidateVisual(); // To force the FlyleafSharedOverlay (if any) redraw on-top
            }
            else if (Surface.Topmost || DetachedTopMost) // Bring to front (in Desktop, above windows bar)
            {
                Surface.Topmost = false;
                Surface.Topmost = true;
            }

            UpdateCurRatio();

            // TBR: CornerRadius background has issue it's like a mask color?
            if (Player != null)
                Player.renderer.CornerRadius = CornerRadius;

            if (CornerRadius != zeroCornerRadius)
                ((Border)Surface.Content).CornerRadius = CornerRadius;

            if (!IsStandAlone) //when play with alpha video and not standalone, we need to set window state to normal last, otherwise it will be lost the background
                Surface.WindowState = WindowState.Normal;
        }
    }
    public void SetRect(ref Rect rect)
        => SetWindowPos(SurfaceHandle, IntPtr.Zero, (int)Math.Round(rect.X * DpiX), (int)Math.Round(rect.Y * DpiY), (int)Math.Round(rect.Width * DpiX), (int)Math.Round(rect.Height * DpiY),
            (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE));

    private void SetRectOverlay(object sender, SizeChangedEventArgs e)
    {
        if (Overlay != null)
            SetWindowPos(OverlayHandle, IntPtr.Zero, 0, 0, (int)Math.Round(Surface.ActualWidth * DpiX), (int)Math.Round(Surface.ActualHeight * DpiY),
                (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOACTIVATE));
    }

    public void ResetVisibleRect()
    {
        SetWindowRgn(SurfaceHandle, IntPtr.Zero, true);
        if (Overlay != null)
            SetWindowRgn(OverlayHandle, IntPtr.Zero, true);
    }
    public void SetVisibleRect(ref Rect rect)
    {
        SetWindowRgn(SurfaceHandle, CreateRectRgn((int)Math.Round(rect.X * DpiX), (int)Math.Round(rect.Y * DpiY), (int)Math.Round(rect.Right * DpiX), (int)Math.Round(rect.Bottom * DpiY)), true);
        if (Overlay != null)
            SetWindowRgn(OverlayHandle, CreateRectRgn((int)Math.Round(rect.X * DpiX), (int)Math.Round(rect.Y * DpiY), (int)Math.Round(rect.Right * DpiX), (int)Math.Round(rect.Bottom * DpiY)), true);
    }

    /// <summary>
    /// Disposes the Surface and Overlay Windows and de-assigns the Player
    /// </summary>
    public void Dispose()
    {
        lock (this)
        {
            if (Disposed)
                return;

            // Disposes SwapChain Only
            Player = null;
            ReplicaPlayer = null;
            Disposed = true;

            DataContextChanged  -= Host_DataContextChanged;
            LayoutUpdated       -= Host_LayoutUpdated;
            IsVisibleChanged    -= Host_IsVisibleChanged;
            Loaded     			-= Host_Loaded;

            if (Overlay != null)
            {
                if (isMouseBindingsSubscribedOverlay)
                    SetMouseOverlay();

                Overlay.IsVisibleChanged-= OverlayStandAlone_IsVisibleChanged;
                Overlay.KeyUp           -= Overlay_KeyUp;
                Overlay.KeyDown         -= Overlay_KeyDown;
                Overlay.Closed          -= Overlay_Closed;
                Overlay.Drop            -= Overlay_Drop;
                Overlay.DragEnter       -= Overlay_DragEnter;
            }

            if (Surface != null)
            {
                if (isMouseBindingsSubscribedSurface)
                    SetMouseSurface();

                Surface.Closed      -= Surface_Closed;
                Surface.Closing     -= Surface_Closing;
                Surface.KeyDown     -= Surface_KeyDown;
                Surface.KeyUp       -= Surface_KeyUp;
                Surface.Drop        -= Surface_Drop;
                Surface.DragEnter   -= Surface_DragEnter;
                Surface.StateChanged-= Surface_StateChanged;
                Surface.SizeChanged -= SetRectOverlay;

                // If not shown yet app will not close properly
                if (!surfaceClosed)
                {
                    Surface.Owner = null;
                    SetParent(SurfaceHandle, IntPtr.Zero);
                    Surface.Width = Surface.Height = 1;
                    Surface.Show();
                    if (!overlayClosed)
                        Overlay?.Show();
                    Surface.Close();
                }
            }

            if (Owner != null)
                Owner.SizeChanged -= Owner_SizeChanged;

            Surface = null;
            Overlay = null;
            Owner   = null;

            SurfaceHandle   = IntPtr.Zero;
            OverlayHandle   = IntPtr.Zero;
            OwnerHandle     = IntPtr.Zero;

            Log.Debug("Disposed");
        }
    }

    public bool Player_CanHideCursor() => (Surface != null && Surface.IsMouseOver) ||
                                          (Overlay != null && Overlay.IsActive);
    public bool Player_GetFullScreen() => IsFullScreen;
    public void Player_SetFullScreen(bool value) => IsFullScreen = value;
    public void Player_Disposed() => UIInvokeIfRequired(() => Player = null);
    #endregion
}

public enum AvailableWindows
{
    None, Surface, Overlay, Both
}

public enum AttachedDragMoveOptions
{
    None, Surface, Overlay, Both, SurfaceOwner, OverlayOwner, BothOwner
}

public enum DetachedPositionOptions
{
    Custom, TopLeft, TopCenter, TopRight, CenterLeft, CenterCenter, CenterRight, BottomLeft, BottomCenter, BottomRight
}

using System.Windows;

using Vortice;
using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DirectComposition;
using Vortice.DXGI;

using static FlyleafLib.Utils.NativeMethods;
using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;

namespace FlyleafLib.MediaFramework.MediaRenderer;

public partial class Renderer
{
    ID3D11Texture2D                         backBuffer;
    ID3D11RenderTargetView                  backBufferRtv;
    IDXGISwapChain1                         swapChain;
    IDCompositionDevice                     dCompDevice;
    IDCompositionVisual                     dCompVisual;
    IDCompositionTarget                     dCompTarget;

    const Int32         WM_NCDESTROY= 0x0082;
    const Int32         WM_SIZE     = 0x0005;
    const Int32         WS_EX_NOREDIRECTIONBITMAP
                                    = 0x00200000;
    SubclassWndProc     wndProcDelegate;
    IntPtr              wndProcDelegatePtr;

    // Support Windows 8+
    private SwapChainDescription1 GetSwapChainDesc(int width, int height, bool isComp = false, bool alpha = false)
    {
        if (Device.FeatureLevel < FeatureLevel.Level_10_0 || (!string.IsNullOrWhiteSpace(Config.Video.GPUAdapter) && Config.Video.GPUAdapter.ToUpper() == "WARP"))
        {
            return new()
            {
                BufferUsage = Usage.RenderTargetOutput,
                Format      = Config.Video.Swap10Bit ? Format.R10G10B10A2_UNorm : Format.B8G8R8A8_UNorm,
                Width       = (uint)width,
                Height      = (uint)height,
                AlphaMode   = AlphaMode.Ignore,
                SwapEffect  = isComp ? SwapEffect.FlipSequential : SwapEffect.Discard, // will this work for warp?
                Scaling     = Scaling.Stretch,
                BufferCount = 1,
                SampleDescription = new SampleDescription(1, 0)
            };
        }
        else
        {
            SwapEffect swapEffect = isComp ? SwapEffect.FlipSequential : Environment.OSVersion.Version.Major >= 10 ? SwapEffect.FlipDiscard : SwapEffect.FlipSequential;

            return new()
            {
                BufferUsage = Usage.RenderTargetOutput,
                Format      = Config.Video.Swap10Bit ? Format.R10G10B10A2_UNorm : (Config.Video.SwapForceR8G8B8A8 ? Format.R8G8B8A8_UNorm : Format.B8G8R8A8_UNorm),
                Width       = (uint)width,
                Height      = (uint)height,
                AlphaMode   = alpha  ? AlphaMode.Premultiplied : AlphaMode.Ignore,
                SwapEffect  = swapEffect,
                Scaling     = isComp ? Scaling.Stretch : Scaling.None,
                BufferCount = swapEffect == SwapEffect.FlipDiscard ? Math.Min(Config.Video.SwapBuffers, 2) : Config.Video.SwapBuffers,
                SampleDescription = new SampleDescription(1, 0),
            };
        }
    }

    internal void InitializeSwapChain(IntPtr handle)
    {
        lock (lockDevice)
        {
            if (!SCDisposed)
                DisposeSwapChain();

            if (Disposed && parent == null)
                Initialize(false);

            ControlHandle   = handle;
            RECT rect       = new();
            GetWindowRect(ControlHandle,ref rect);
            ControlWidth    = rect.Right  - rect.Left;
            ControlHeight   = rect.Bottom - rect.Top;

            try
            {
                if (cornerRadius == zeroCornerRadius)
                {
                    Log.Info($"Initializing {(Config.Video.Swap10Bit ? "10-bit" : "8-bit")} swap chain with {Config.Video.SwapBuffers} buffers [Handle: {handle}]");
                    swapChain = Engine.Video.Factory.CreateSwapChainForHwnd(Device, handle, GetSwapChainDesc(ControlWidth, ControlHeight));
                }
                else
                {
                    Log.Info($"Initializing {(Config.Video.Swap10Bit ? "10-bit" : "8-bit")} composition swap chain with {Config.Video.SwapBuffers} buffers [Handle: {handle}]");
                    swapChain = Engine.Video.Factory.CreateSwapChainForComposition(Device, GetSwapChainDesc(ControlWidth, ControlHeight, true, true));
                    using (var dxgiDevice = Device.QueryInterface<IDXGIDevice>())
                    dCompDevice = DComp.DCompositionCreateDevice<IDCompositionDevice>(dxgiDevice);
                    dCompDevice.CreateTargetForHwnd(handle, false, out dCompTarget).CheckError();
                    dCompDevice.CreateVisual(out dCompVisual).CheckError();
                    dCompVisual.SetContent(swapChain).CheckError();
                    dCompTarget.SetRoot(dCompVisual).CheckError();
                    dCompDevice.Commit().CheckError();

                    int styleEx = GetWindowLong(handle, (int)WindowLongFlags.GWL_EXSTYLE).ToInt32() | WS_EX_NOREDIRECTIONBITMAP;
                    SetWindowLong(handle, (int)WindowLongFlags.GWL_EXSTYLE, new IntPtr(styleEx));
                }
            }
            catch (Exception e)
            {
                if (string.IsNullOrWhiteSpace(Config.Video.GPUAdapter) || Config.Video.GPUAdapter.ToUpper() != "WARP")
                {
                    try { if (Device != null) Log.Warn($"Device Remove Reason = {Device.DeviceRemovedReason.Description}"); } catch { } // For troubleshooting

                    Log.Warn($"[SwapChain] Initialization failed ({e.Message}). Failling back to WARP device.");
                    Config.Video.GPUAdapter = "WARP";
                    Flush();
                }
                else
                {
                    ControlHandle = IntPtr.Zero;
                    Log.Error($"[SwapChain] Initialization failed ({e.Message})");
                }

                return;
            }

            backBuffer      = swapChain.GetBuffer<ID3D11Texture2D>(0);
            backBufferRtv   = Device.CreateRenderTargetView(backBuffer);
            SCDisposed      = false;

            if (!isFlushing) // avoid calling UI thread during Player.Stop
            {
                // SetWindowSubclass seems to require UI thread when RemoveWindowSubclass does not (docs are not mentioning this?)
                if (System.Threading.Thread.CurrentThread.ManagedThreadId == System.Windows.Application.Current.Dispatcher.Thread.ManagedThreadId)
                    SetWindowSubclass(ControlHandle, wndProcDelegatePtr, UIntPtr.Zero, UIntPtr.Zero);
                else
                    Utils.UI(() => SetWindowSubclass(ControlHandle, wndProcDelegatePtr, UIntPtr.Zero, UIntPtr.Zero));
            }

            Engine.Video.Factory.MakeWindowAssociation(ControlHandle, WindowAssociationFlags.IgnoreAll);

            ResizeBuffers(ControlWidth, ControlHeight); // maybe not required (only for vp)?
        }
    }
    internal void InitializeWinUISwapChain() // TODO: width/height directly here
    {
        lock (lockDevice)
        {
            if (!SCDisposed)
                DisposeSwapChain();

            if (Disposed)
                Initialize(false);

            Log.Info($"Initializing {(Config.Video.Swap10Bit ? "10-bit" : "8-bit")} swap chain with {Config.Video.SwapBuffers} buffers");

            try
            {
                swapChain = Engine.Video.Factory.CreateSwapChainForComposition(Device, GetSwapChainDesc(1, 1, true));
            }
            catch (Exception e)
            {
                Log.Error($"Initialization failed [{e.Message}]");

                // TODO fallback to WARP?

                SwapChainWinUIClbk?.Invoke(null);
                return;
            }

            backBuffer      = swapChain.GetBuffer<ID3D11Texture2D>(0);
            backBufferRtv   = Device.CreateRenderTargetView(backBuffer);
            SCDisposed      = false;
            ResizeBuffers(1, 1);

            SwapChainWinUIClbk?.Invoke(swapChain.QueryInterface<IDXGISwapChain2>());
        }
    }

    public void DisposeSwapChain()
    {
        lock (lockDevice)
        {
            if (SCDisposed)
                return;

            SCDisposed = true;

            // Clear Screan
            if (!Disposed && swapChain != null)
            {
                try
                {
                    context.ClearRenderTargetView(backBufferRtv, Config.Video._BackgroundColor);
                    swapChain.Present(Config.Video.VSync, PresentFlags.None);
                }
                catch { }
            }

            Log.Info($"Destroying swap chain [Handle: {ControlHandle}]");

            // Unassign renderer's WndProc if still there and re-assign the old one
            if (ControlHandle != IntPtr.Zero)
            {
                if (!isFlushing) // SetWindowSubclass requires UI thread so avoid calling it on flush (Player.Stop)
                    RemoveWindowSubclass(ControlHandle, wndProcDelegatePtr, UIntPtr.Zero);
                ControlHandle = IntPtr.Zero;
            }

            if (SwapChainWinUIClbk != null)
            {
                SwapChainWinUIClbk.Invoke(null);
                SwapChainWinUIClbk = null;
                swapChain?.Release(); // TBR: SwapChainPanel (SCP) should be disposed and create new instance instead (currently used from Template)
            }

            dCompVisual?.Dispose();
            dCompTarget?.Dispose();
            dCompDevice?.Dispose();
            dCompVisual = null;
            dCompTarget = null;
            dCompDevice = null;

            vpov?.Dispose();
            backBufferRtv?.Dispose();
            backBuffer?.Dispose();
            swapChain?.Dispose();

            if (Device != null)
                context?.Flush();
        }
    }

    public void RemoveViewportOffsets(ref Point p)
    {
        p.X -= (SideXPixels / 2 + PanXOffset);
        p.Y -= (SideYPixels / 2 + PanYOffset);
    }
    public bool IsPointWithInViewPort(Point p)
        => p.X >= GetViewport.X && p.X < GetViewport.X + GetViewport.Width && p.Y >= GetViewport.Y && p.Y < GetViewport.Y + GetViewport.Height;

    public static double GetCenterPoint(double zoom, double offset)
        => zoom == 1 ? offset : offset / (zoom - 1); // possible bug when zoom = 1 (noticed in out of bounds zoom out)

    /// <summary>
    /// Zooms in a way that the specified point before zoom will be at the same position after zoom
    /// </summary>
    /// <param name="p"></param>
    /// <param name="zoom"></param>
    public void ZoomWithCenterPoint(Point p, double zoom)
    {
        /* Notes
         *
         * Zoomed Point (ZP)    // the current point in a -possible- zoomed viewport
         * Zoom (Z)
         * Unzoomed Point (UP)  // the actual pixel of the current point
         * Viewport Point (VP)
         * Center Point (CP)
         *
         * UP = (VP + ZP) / Z =>
         * ZP = (UP * Z) - VP
         * CP = VP / (ZP - 1) (when UP = ZP)
         */

        if (!IsPointWithInViewPort(p))
        {
            SetZoom(zoom);
            return;
        }

        Point viewport = new(GetViewport.X, GetViewport.Y);
        RemoveViewportOffsets(ref viewport);
        RemoveViewportOffsets(ref p);

        // Finds the required center point so that p will have the same pixel after zoom
        Point zoomCenter = new(
            GetCenterPoint(zoom, ((p.X - viewport.X) / (this.zoom / zoom)) - p.X) / (GetViewport.Width / this.zoom),
            GetCenterPoint(zoom, ((p.Y - viewport.Y) / (this.zoom / zoom)) - p.Y) / (GetViewport.Height / this.zoom));

        SetZoomAndCenter(zoom, zoomCenter);
    }

    public void SetViewport(bool refresh = true)
    {
        float ratio;
        int x, y, newWidth, newHeight, xZoomPixels, yZoomPixels;

        if (Config.Video.AspectRatio == AspectRatio.Keep)
            ratio = curRatio;
        else ratio = Config.Video.AspectRatio == AspectRatio.Fill
            ? ControlWidth / (float)ControlHeight
            : Config.Video.AspectRatio == AspectRatio.Custom ? Config.Video.CustomAspectRatio.Value : Config.Video.AspectRatio.Value;

        if (ratio <= 0)
            ratio = 1;

        if (actualRotation == 90 || actualRotation == 270)
            ratio = 1 / ratio;

        if (ratio < ControlWidth / (float) ControlHeight)
        {
            newHeight = (int)(ControlHeight * zoom);
            newWidth = (int)(newHeight * ratio);

            SideXPixels = newWidth > ControlWidth && false ? 0 : (int) (ControlWidth - (ControlHeight * ratio)); // TBR
            SideYPixels = 0;

            y = PanYOffset;
            x = PanXOffset + SideXPixels / 2;

            yZoomPixels = newHeight - ControlHeight;
            xZoomPixels = newWidth - (ControlWidth - SideXPixels);
        }
        else
        {
            newWidth = (int)(ControlWidth * zoom);
            newHeight = (int)(newWidth / ratio);

            SideYPixels = newHeight > ControlHeight && false ? 0 : (int) (ControlHeight - (ControlWidth / ratio));
            SideXPixels = 0;

            x = PanXOffset;
            y = PanYOffset + SideYPixels / 2;

            xZoomPixels = newWidth - ControlWidth;
            yZoomPixels = newHeight - (ControlHeight - SideYPixels);
        }

        GetViewport = new(x - xZoomPixels * (float)zoomCenter.X, y - yZoomPixels * (float)zoomCenter.Y, newWidth, newHeight);
        ViewportChanged?.Invoke(this, new());

        if (videoProcessor == VideoProcessors.D3D11)
        {
            RawRect src, dst;

            if (GetViewport.Width < 1 || GetViewport.X + GetViewport.Width <= 0 || GetViewport.X >= ControlWidth || GetViewport.Y + GetViewport.Height <= 0 || GetViewport.Y >= ControlHeight)
            { // Out of screen
                src = new RawRect();
                dst = new RawRect();
            }
            else
            {
                int cropLeft    = GetViewport.X < 0 ? (int) GetViewport.X * -1 : 0;
                int cropRight   = GetViewport.X + GetViewport.Width > ControlWidth ? (int) (GetViewport.X + GetViewport.Width - ControlWidth) : 0;
                int cropTop     = GetViewport.Y < 0 ? (int) GetViewport.Y * -1 : 0;
                int cropBottom  = GetViewport.Y + GetViewport.Height > ControlHeight ? (int) (GetViewport.Y + GetViewport.Height - ControlHeight) : 0;

                dst = new RawRect(Math.Max((int)GetViewport.X, 0), Math.Max((int)GetViewport.Y, 0), Math.Min((int)GetViewport.Width + (int)GetViewport.X, ControlWidth), Math.Min((int)GetViewport.Height + (int)GetViewport.Y, ControlHeight));

                if (_RotationAngle == 90)
                {
                    src = new RawRect(
                        (int) (cropTop * (VideoRect.Right / GetViewport.Height)),
                        (int) (cropRight * (VideoRect.Bottom / GetViewport.Width)),
                        VideoRect.Right - (int) (cropBottom * VideoRect.Right / GetViewport.Height),
                        VideoRect.Bottom - (int) (cropLeft * VideoRect.Bottom / GetViewport.Width));
                }
                else if (_RotationAngle == 270)
                {
                    src = new RawRect(
                        (int) (cropBottom * VideoRect.Right / GetViewport.Height),
                        (int) (cropLeft * VideoRect.Bottom / GetViewport.Width),
                        VideoRect.Right - (int) (cropTop * VideoRect.Right / GetViewport.Height),
                        VideoRect.Bottom - (int) (cropRight * VideoRect.Bottom / GetViewport.Width));
                }
                else if (_RotationAngle == 180)
                {
                    src = new RawRect(
                        (int) (cropRight * VideoRect.Right / GetViewport.Width),
                        (int) (cropBottom * VideoRect.Bottom /GetViewport.Height),
                        VideoRect.Right - (int) (cropLeft * VideoRect.Right / GetViewport.Width),
                        VideoRect.Bottom - (int) (cropTop * VideoRect.Bottom / GetViewport.Height));
                }
                else
                {
                    src = new RawRect(
                        (int) (cropLeft * VideoRect.Right / GetViewport.Width),
                        (int) (cropTop * VideoRect.Bottom / GetViewport.Height),
                        VideoRect.Right - (int) (cropRight * VideoRect.Right / GetViewport.Width),
                        VideoRect.Bottom - (int) (cropBottom * VideoRect.Bottom / GetViewport.Height));
                }
            }

            vc.VideoProcessorSetStreamSourceRect(vp, 0, true, src);
            vc.VideoProcessorSetStreamDestRect  (vp, 0, true, dst);
            vc.VideoProcessorSetOutputTargetRect(vp, true, new RawRect(0, 0, ControlWidth, ControlHeight));
        }

        if (refresh)
            Present();
    }
    public void ResizeBuffers(int width, int height)
    {
        lock (lockDevice)
        {
            if (SCDisposed)
                return;

            ControlWidth = width;
            ControlHeight = height;

            backBufferRtv.Dispose();
            vpov?.Dispose();
            backBuffer.Dispose();
            swapChain.ResizeBuffers(0, (uint)ControlWidth, (uint)ControlHeight, Format.Unknown, SwapChainFlags.None);
            UpdateCornerRadius();
            backBuffer = swapChain.GetBuffer<ID3D11Texture2D>(0);
            backBufferRtv = Device.CreateRenderTargetView(backBuffer);
            if (videoProcessor == VideoProcessors.D3D11)
                vd1.CreateVideoProcessorOutputView(backBuffer, vpe, vpovd, out vpov);

            SetViewport();
        }
    }

    internal void UpdateCornerRadius()
    {
        if (dCompDevice == null)
            return;

        dCompDevice.CreateRectangleClip(out var clip).CheckError();
        clip.SetLeft(0);
        clip.SetRight(ControlWidth);
        clip.SetTop(0);
        clip.SetBottom(ControlHeight);
        clip.SetTopLeftRadiusX((float)cornerRadius.TopLeft);
        clip.SetTopLeftRadiusY((float)cornerRadius.TopLeft);
        clip.SetTopRightRadiusX((float)cornerRadius.TopRight);
        clip.SetTopRightRadiusY((float)cornerRadius.TopRight);
        clip.SetBottomLeftRadiusX((float)cornerRadius.BottomLeft);
        clip.SetBottomLeftRadiusY((float)cornerRadius.BottomLeft);
        clip.SetBottomRightRadiusX((float)cornerRadius.BottomRight);
        clip.SetBottomRightRadiusY((float)cornerRadius.BottomRight);
        dCompVisual.SetClip(clip).CheckError();
        clip.Dispose();
        dCompDevice.Commit().CheckError();
    }

    private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData)
    {
        switch (msg)
        {
            case WM_NCDESTROY:
                if (SCDisposed)
                    RemoveWindowSubclass(ControlHandle, wndProcDelegatePtr, UIntPtr.Zero);
                else
                    DisposeSwapChain();
                break;

            case WM_SIZE:
                ResizeBuffers(SignedLOWORD(lParam), SignedHIWORD(lParam));
                break;
        }

        return DefSubclassProc(hWnd, msg, wParam, lParam);
    }
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using FlyleafLib.MediaPlayer.Translation.Services;
using LLPlayer.Extensions;
using MaterialDesignThemes.Wpf;
using Vortice.Mathematics;
using Color = System.Windows.Media.Color;
using Colors = System.Windows.Media.Colors;
using Size = System.Windows.Size;

namespace LLPlayer.Services;

public class AppConfig : Bindable
{
    private FlyleafManager FL = null!;

    public void Initialize(FlyleafManager fl)
    {
        FL = fl;
        Loaded = true;

        Subs.Initialize(this, fl);
    }

    public string Version { get; set; } = "";

    /// <summary>
    /// State to skip the setter run when reading JSON
    /// </summary>
    [JsonIgnore]
    public bool Loaded { get; private set; }

    public void FlyleafHostLoaded()
    {
        Subs.FlyleafHostLoaded();

        // Ensure that FlyeafHost reflects the configuration values when restoring the configuration.
        FL.FlyleafHost!.ActivityTimeout = FL.Config.ActivityTimeout;
    }

    public AppConfigSubs Subs { get; set => Set(ref field, value); } = new();

    public static AppConfig Load(string path)
    {
        AppConfig config = JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(path), GetJsonSerializerOptions())!;

        return config;
    }

    public void Save(string path)
    {
        Version = App.Version;
        File.WriteAllText(path, JsonSerializer.Serialize(this, GetJsonSerializerOptions()));

        Subs.SaveAfter();
    }

    public AppConfigTheme Theme { get; set => Set(ref field, value); } = new();

    public bool AlwaysOnTop { get; set => Set(ref field, value); }

    [JsonIgnore]
    public double ScreenWidth
    {
        private get;
        set => Set(ref field, value);
    }

    // Video Screen Height (including black background), without titlebar height
    [JsonIgnore]
    public double ScreenHeight
    {
        internal get;
        set
        {
            if (Set(ref field, value))
            {
                Subs.UpdateSubsConfig();
            }
        }
    }

    public bool IsDarkTitlebar { get; set => Set(ref field, value); } = true;

    public int ActivityTimeout
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (Loaded && FL.FlyleafHost != null)
                {
                    FL.FlyleafHost.ActivityTimeout = value;
                }
            }
        }
    } = 1200;

    public bool ShowSidebar { get; set => Set(ref field, value); } = true;

    [JsonIgnore]
    public bool ShowDebug
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                FL.PlayerConfig.Player.Stats = value;
            }
        }
    }

    #region FlyleafBar
    public bool SeekBarShowOnlyMouseOver { get; set => Set(ref field, value); } = false;
    public int SeekBarFadeInTimeMs { get; set => Set(ref field, value); } = 80;
    public int SeekBarFadeOutTimeMs { get; set => Set(ref field, value); } = 150;
    #endregion

    #region Mouse
    public bool MouseSingleClickToPlay { get; set => Set(ref field, value); } = true;
    public bool MouseDoubleClickToFullScreen { get; set => Set(ref field, value); }
    public bool MouseWheelToVolumeUpDown { get; set => Set(ref field, value); } = true;
    #endregion

    // TODO: L: should be move to AppConfigSubs?
    public bool SidebarLeft
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(SidebarFlowDirection));
            }
        }
    }

    [JsonIgnore]
    public FlowDirection SidebarFlowDirection => !SidebarLeft ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;

    public int SidebarWidth { get; set => Set(ref field, value); } = 300;

    public int SidebarSubPadding { get; set => Set(ref field, value); } = 5;

    [JsonIgnore]
    public bool SidebarShowSecondary { get; set => Set(ref field, value); }

    public bool SidebarShowOriginalText { get; set => Set(ref field, value); }

    public bool SidebarTextMask { get; set => Set(ref field, value); }

    [JsonIgnore]
    public bool SidebarSearchActive { get; set => Set(ref field, value); }

    public string SidebarFontFamily { get; set => Set(ref field, value); } = "Segoe UI";

    public double SidebarFontSize { get; set => Set(ref field, value); } = 16;

    public string SidebarFontWeight { get; set => Set(ref field, value); } = FontWeights.Normal.ToString();

    public static JsonSerializerOptions GetJsonSerializerOptions()
    {
        Dictionary<string, Type> typeMappingMenuAction = new()
        {
            { nameof(ClipboardMenuAction), typeof(ClipboardMenuAction) },
            { nameof(ClipboardAllMenuAction), typeof(ClipboardAllMenuAction) },
            { nameof(SearchMenuAction), typeof(SearchMenuAction) },
        };

        Dictionary<string, Type> typeMappingTranslateSettings = new()
        {
            { nameof(GoogleV1TranslateSettings), typeof(GoogleV1TranslateSettings) },
            { nameof(DeepLTranslateSettings), typeof(DeepLTranslateSettings) },
            { nameof(DeepLXTranslateSettings), typeof(DeepLXTranslateSettings) },
            { nameof(OllamaTranslateSettings), typeof(OllamaTranslateSettings) },
            { nameof(LMStudioTranslateSettings), typeof(LMStudioTranslateSettings) },
            { nameof(KoboldCppTranslateSettings), typeof(KoboldCppTranslateSettings) },
            { nameof(OpenAITranslateSettings), typeof(OpenAITranslateSettings) },
            { nameof(OpenAILikeTranslateSettings), typeof(OpenAILikeTranslateSettings) },
            { nameof(ClaudeTranslateSettings), typeof(ClaudeTranslateSettings) },
            { nameof(LiteLLMTranslateSettings), typeof(LiteLLMTranslateSettings) }
        };

        JsonSerializerOptions jsonOptions = new()
        {
            WriteIndented = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            Converters =
            {
                new JsonStringEnumConverter(),
                // TODO: L: should separate converters for different types of config?
                new JsonInterfaceConcreteConverter<IMenuAction>(typeMappingMenuAction),
                new JsonInterfaceConcreteConverter<ITranslateSettings>(typeMappingTranslateSettings),
                new ColorHexJsonConverter()
            }
        };

        return jsonOptions;
    }
}

public class AppConfigSubs : Bindable
{
    private AppConfig _rootConfig = null!;
    private FlyleafManager FL = null!;

    [JsonIgnore]
    public bool Loaded { get; private set; }

    public void Initialize(AppConfig rootConfig, FlyleafManager fl)
    {
        _rootConfig = rootConfig;
        FL = fl;
        Loaded = true;

        // Save the initial value of the position for reset.
        _subsPositionInitial = SubsPosition;

        // register event handler
        var pSubsAutoCopy = SubsAutoTextCopy;
        SubsAutoTextCopy = false;
        SubsAutoTextCopy = pSubsAutoCopy;
    }

    public void FlyleafHostLoaded()
    {
        Viewport = FL.Player.renderer.GetViewport;

        FL.Player.renderer.ViewportChanged += (sender, args) =>
        {
            Utils.UIIfRequired(() =>
            {
                Viewport = FL.Player.renderer.GetViewport;
            });
        };
    }

    public void SaveAfter()
    {
        // Update initial value
        _subsPositionInitial = SubsPosition;
    }

    [JsonIgnore]
    public Viewport Viewport
    {
        get;
        private set
        {
            var prev = Viewport;
            if (Set(ref field, value))
            {
                if ((int)prev.Width != (int)value.Width)
                {
                    // update font size if width changed
                    OnPropertyChanged(nameof(SubsFontSizeFix));
                    OnPropertyChanged(nameof(SubsFontSize2Fix));
                }

                if ((int)prev.Height != (int)value.Height ||
                    (int)prev.Y != (int)value.Y)
                {
                    // update font margin/distance if height/Y changed
                    UpdateSubsConfig();
                }
            }
        }
    }

    public bool SubsUseSeparateFonts
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                // update subtitles
                UpdateSecondaryFonts();

                OnPropertyChanged(nameof(SubsFontColor2Fix));
                OnPropertyChanged(nameof(SubsBackgroundBrush2));
            }
        }
    }

    internal void UpdateSecondaryFonts()
    {
        // update subtitles display
        OnPropertyChanged(nameof(SubsFontFamily2Fix));
        OnPropertyChanged(nameof(SubsFontStretch2Fix));
        OnPropertyChanged(nameof(SubsFontWeight2Fix));
        OnPropertyChanged(nameof(SubsFontStyle2Fix));

        CmdResetSubsFont2!.RaiseCanExecuteChanged();
    }

    // Primary Subtitle Size
    public double SubsFontSize
    {
        get;
        set
        {
            if (value <= 0)
            {
                return;
            }

            if (Set(ref field, Math.Round(value)))
            {
                OnPropertyChanged(nameof(SubsFontSizeFix));
                CmdResetSubsFontSize2!.RaiseCanExecuteChanged();
            }
        }
    } = 44;

    [JsonIgnore]
    public double SubsFontSizeFix => GetFixFontSize(SubsFontSize);

    // Secondary Subtitle Size
    public double SubsFontSize2
    {
        get;
        set
        {
            if (value <= 0)
            {
                return;
            }

            if (Set(ref field, Math.Round(value)))
            {
                OnPropertyChanged(nameof(SubsFontSize2Fix));
                CmdResetSubsFontSize2!.RaiseCanExecuteChanged();
            }
        }
    } = 44;

    [JsonIgnore]
    public double SubsFontSize2Fix => GetFixFontSize(SubsFontSize2);

    private double GetFixFontSize(double fontSize)
    {
        double scaleFactor = Viewport.Width / 1920;
        double size = fontSize * scaleFactor;
        if (size > 0)
        {
            return size;
        }

        return fontSize;
    }

    private const string DefaultFont = "Segoe UI";
    public string SubsFontFamily { get; set => Set(ref field, value); } = DefaultFont;
    public string SubsFontStretch { get; set => Set(ref field, value); } = FontStretches.Normal.ToString();
    public string SubsFontWeight { get; set => Set(ref field, value); } = FontWeights.Bold.ToString();
    public string SubsFontStyle { get; set => Set(ref field, value); } = FontStyles.Normal.ToString();
    public Color SubsFontColor
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(SubsFontColor2Fix));
                CmdResetSubsFontColor2!.RaiseCanExecuteChanged();
            }
        }
    } = Colors.White;

    [JsonIgnore]
    public string SubsFontFamily2Fix => SubsUseSeparateFonts ? SubsFontFamily2 : SubsFontFamily;
    public string SubsFontFamily2 { get; set => Set(ref field, value); } = DefaultFont;

    [JsonIgnore]
    public string SubsFontStretch2Fix => SubsUseSeparateFonts ? SubsFontStretch2 : SubsFontStretch;
    public string SubsFontStretch2 { get; set => Set(ref field, value); } = FontStretches.Normal.ToString();

    [JsonIgnore]
    public string SubsFontWeight2Fix => SubsUseSeparateFonts ? SubsFontWeight2 : SubsFontWeight;
    public string SubsFontWeight2 { get; set => Set(ref field, value); } = FontWeights.SemiBold.ToString(); // change from bold

    [JsonIgnore]
    public string SubsFontStyle2Fix => SubsUseSeparateFonts ? SubsFontStyle2 : SubsFontStyle;
    public string SubsFontStyle2 { get; set => Set(ref field, value); } = FontStyles.Normal.ToString();

    [JsonIgnore]
    public Color SubsFontColor2Fix => SubsUseSeparateFonts ? SubsFontColor2 : SubsFontColor;
    public Color SubsFontColor2
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(SubsFontColor2Fix));
                CmdResetSubsFontColor2!.RaiseCanExecuteChanged();
            }
        }
    } = Color.FromRgb(231, 231, 231); // #E7E7E7

    public Color SubsStrokeColor { get; set => Set(ref field, value); } = Colors.Black;
    public double SubsStrokeThickness { get; set => Set(ref field, value); } = 3.2;

    public Color SubsBackgroundColor
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(SubsBackgroundBrush));
                OnPropertyChanged(nameof(SubsBackgroundBrush2));
            }
        }
    } = Colors.Black;

    public double SubsBackgroundOpacity
    {
        get;
        set
        {
            if (value < 0.0 || value > 1.0)
            {
                return;
            }

            if (Set(ref field, Math.Round(value, 2)))
            {
                OnPropertyChanged(nameof(SubsBackgroundBrush));
                OnPropertyChanged(nameof(SubsBackgroundBrush2));
                CmdResetSubsBackgroundOpacity2!.RaiseCanExecuteChanged();
            }
        }
    } = 0; // default no background

    [JsonIgnore]
    public SolidColorBrush SubsBackgroundBrush
    {
        get
        {
            byte alpha = (byte)(SubsBackgroundOpacity * 255);
            return new SolidColorBrush(Color.FromArgb(alpha, SubsBackgroundColor.R, SubsBackgroundColor.G, SubsBackgroundColor.B));
        }
    }

    [JsonIgnore]
    private double SubsBackgroundOpacity2Fix => SubsUseSeparateFonts ? SubsBackgroundOpacity2 : SubsBackgroundOpacity;
    public double SubsBackgroundOpacity2
    {
        get;
        set
        {
            if (value < 0.0 || value > 1.0)
            {
                return;
            }

            if (Set(ref field, Math.Round(value, 2)))
            {
                OnPropertyChanged(nameof(SubsBackgroundBrush2));
                CmdResetSubsBackgroundOpacity2!.RaiseCanExecuteChanged();
            }
        }
    } = 0;

    [JsonIgnore]
    public SolidColorBrush SubsBackgroundBrush2
    {
        get
        {
            byte alpha = (byte)(SubsBackgroundOpacity2Fix * 255);
            return new SolidColorBrush(Color.FromArgb(alpha, SubsBackgroundColor.R, SubsBackgroundColor.G, SubsBackgroundColor.B));
        }
    }

    [JsonIgnore]
    public DelegateCommand? CmdResetSubsFontSize2 => field ??= new(() =>
    {
        SubsFontSize2 = SubsFontSize;
    }, () => SubsFontSize2 != SubsFontSize);

    [JsonIgnore]
    public DelegateCommand? CmdResetSubsFont2 => field ??= new(() =>
    {
        SubsFontFamily2 = SubsFontFamily;
        SubsFontStretch2 = SubsFontStretch;
        SubsFontWeight2 = SubsFontWeight;
        SubsFontStyle2 = SubsFontStyle;

        UpdateSecondaryFonts();
    }, () =>
        SubsFontFamily2 != SubsFontFamily ||
        SubsFontStretch2 != SubsFontStretch ||
        SubsFontWeight2 != SubsFontWeight ||
        SubsFontStyle2 != SubsFontStyle
    );

    [JsonIgnore]
    public DelegateCommand? CmdResetSubsFontColor2 => field ??= new(() =>
    {
        SubsFontColor2 = SubsFontColor;
    }, () => SubsFontColor2 != SubsFontColor);

    [JsonIgnore]
    public DelegateCommand? CmdResetSubsBackgroundOpacity2 => field ??= new(() =>
    {
        SubsBackgroundOpacity2 = SubsBackgroundOpacity;
    }, () => SubsBackgroundOpacity2 != SubsBackgroundOpacity);

    [JsonIgnore]
    public Size SubsPanelSize
    {
        private get;
        set
        {
            if (Set(ref field, value))
            {
                UpdateSubsMargin();
            }
        }
    }

    private bool _isSubsOverflowBottom;

    [JsonIgnore]
    public Thickness SubsMargin
    {
        get;
        set => Set(ref field, value);
    }

    private double _subsPositionInitial;
    public void ResetSubsPosition()
    {
        SubsPosition = _subsPositionInitial;
    }

    #region Offsets

    public double SubsPositionOffset { get; set => Set(ref field, value); } = 2;
    public int SubsFontSizeOffset { get; set => Set(ref field, value); } = 2;
    public double SubsBitmapScaleOffset { get; set => Set(ref field, value); } = 4;
    public double SubsDistanceOffset { get; set => Set(ref field, value); } = 5;

    #endregion

    // -25%-150%
    // Allow some going up and down from ViewPort
    public double SubsPosition
    {
        get;
        set
        {
            if (_isSubsOverflowBottom && SubsPanelSize.Height > 0 && field < value)
            {
                // Prohibit going further down when it overflows below.
                return;
            }

            if (value < -25 || value > 150)
            {
                return;
            }

            if (Set(ref field, Math.Round(value)))
            {
                UpdateSubsMargin();
            }
        }
    } = 85;

    public SubPositionAlignment SubsPositionAlignment
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                UpdateSubsConfig();
            }
        }
    } = SubPositionAlignment.Center;

    public SubPositionAlignment SubsPositionAlignmentWhenDual
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                UpdateSubsConfig();
            }
        }
    } = SubPositionAlignment.Top;

    // 0%-100%
    public double SubsFixOverflowMargin
    {
        get;
        set
        {
            if (value < 0.0 || value > 100.0)
            {
                return;
            }

            Set(ref field, value);
        }
    } = 3.0;

    [JsonIgnore]
    public double SubsDistanceFix { get; set => Set(ref field, value); }

    public double SubsDistance
    {
        get;
        set
        {
            if (value < 1)
            {
                return;
            }

            if (Set(ref field, Math.Round(value)))
            {
                UpdateSubsDistance();
            }
        }
    } = 16;

    public double SubsSeparatorMaxWidth { get; set => Set(ref field, value); } = 280;
    public double SubsSeparatorOpacity
    {
        get;
        set
        {
            if (value < 0.0 || value > 1.0)
            {
                return;
            }

            Set(ref field, value);
        }
    } = 0.3;

    public double SubsWidthPercentage
    {
        get;
        set
        {
            if (value < 1.0 || value > 100.0)
            {
                return;
            }

            Set(ref field, value);
        }
    } = 66.0;

    public double SubsMaxWidth
    {
        get;
        set
        {
            if (value < 0)
            {
                return;
            }

            Set(ref field, value);
        }
    } = 0;

    public bool SubsIgnoreLineBreak { get; set => Set(ref field, value); }

    internal void UpdateSubsConfig()
    {
        if (!Loaded)
            return;

        UpdateSubsDistance();
        UpdateSubsMargin();
    }

    private void UpdateSubsDistance()
    {
        if (!Loaded)
            return;

        float scaleFactor = Viewport.Height / 1080;
        double newDistance = SubsDistance * scaleFactor;

        SubsDistanceFix = newDistance;
    }

    private void UpdateSubsMargin()
    {
        if (!Loaded)
            return;

        // Set the margin from the top based on Viewport, not Window
        // Allow going above or below the Viewport
        float offset = Viewport.Y;
        float height = Viewport.Height;

        double marginTop = height * (SubsPosition / 100.0);
        double marginTopFix = marginTop + offset;

        // Adjustment for vertical alignment of subtitles
        SubPositionAlignment alignment = SubsPositionAlignment;
        if (FL.Player.Subtitles[0].Enabled && FL.Player.Subtitles[1].Enabled)
        {
            alignment = SubsPositionAlignmentWhenDual;
        }

        if (alignment == SubPositionAlignment.Center)
        {
            marginTopFix -= SubsPanelSize.Height / 2;
        }
        else if (alignment == SubPositionAlignment.Bottom)
        {
            marginTopFix -= SubsPanelSize.Height;
        }

        // Corrects for off-screen subtitles if they are detected.
        marginTopFix = FixOverflowSubsPosition(marginTopFix);

        SubsMargin = new Thickness(SubsMargin.Left, marginTopFix, SubsMargin.Right, SubsMargin.Bottom);
    }

    /// <summary>
    /// Detects whether subtitles are placed off-screen and corrects them if they appear
    /// </summary>
    /// <param name="marginTop"></param>
    /// <returns></returns>
    private double FixOverflowSubsPosition(double marginTop)
    {
        double subHeight = SubsPanelSize.Height;

        double bottomMargin = Viewport.Height * (SubsFixOverflowMargin / 100.0);

        if (subHeight + marginTop + bottomMargin > _rootConfig.ScreenHeight)
        {
            // It overflowed, so fix it.
            _isSubsOverflowBottom = true;
            double fixedMargin = _rootConfig.ScreenHeight - subHeight - bottomMargin;
            return fixedMargin;
        }

        _isSubsOverflowBottom = false;
        return marginTop;
    }

    public bool SubsExportUTF8WithBom { get; set => Set(ref field, value); } = true;

    public bool SubsAutoTextCopy
    {
        get;
        set
        {
            if (Set(ref field, value) && Loaded)
            {
                if (value)
                {
                    FL.Player.Subtitles[0].Data.PropertyChanged += SubtitleTextOnPropertyChanged;
                    FL.Player.Subtitles[1].Data.PropertyChanged += SubtitleTextOnPropertyChanged;
                }
                else
                {
                    FL.Player.Subtitles[0].Data.PropertyChanged -= SubtitleTextOnPropertyChanged;
                    FL.Player.Subtitles[1].Data.PropertyChanged -= SubtitleTextOnPropertyChanged;
                }
            }
        }
    }

    public SubAutoTextCopyTarget SubsAutoTextCopyTarget { get; set => Set(ref field, value); } = SubAutoTextCopyTarget.Primary;

    private void SubtitleTextOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName != nameof(SubsData.Text))
            return;

        switch (SubsAutoTextCopyTarget)
        {
            case SubAutoTextCopyTarget.All:
                if (FL.Player.Subtitles[0].Data.Text != "" ||
                    FL.Player.Subtitles[1].Data.Text != "")
                {
                    FL.Action.CmdSubsTextCopy.Execute(true);
                }
                break;
            case SubAutoTextCopyTarget.Primary:
                if (FL.Player.Subtitles[0].Data == sender && FL.Player.Subtitles[0].Data.Text != "")
                {
                    FL.Action.CmdSubsPrimaryTextCopy.Execute(true);
                }
                break;
            case SubAutoTextCopyTarget.Secondary:
                if (FL.Player.Subtitles[1].Data == sender && FL.Player.Subtitles[1].Data.Text != "")
                {
                    FL.Action.CmdSubsSecondaryTextCopy.Execute(true);
                }
                break;
        }
    }

    public WordClickAction WordClickActionMethod { get; set => Set(ref field, value); }
    public bool WordCopyOnSelected { get; set => Set(ref field, value); } = true;
    public bool WordLastSearchOnSelected { get; set => Set(ref field, value); } = true;

    public ModifierKeys WordLastSearchOnSelectedModifier { get; set => Set(ref field, value); } = ModifierKeys.Control;
    public ObservableCollection<IMenuAction> WordMenuActions { get; set => Set(ref field, value); } = new(
    [
        new ClipboardMenuAction(),
        new ClipboardAllMenuAction(),
        new SearchMenuAction{ Title = "Search Google", Url = "https://www.google.com/search?q=%w" },
        new SearchMenuAction{ Title = "Search Wiktionary", Url = "https://en.wiktionary.org/wiki/Special:Search?search=%w&go=Look+up" },
        new SearchMenuAction{ Title = "Search Longman", Url = "https://www.ldoceonline.com/search/english/direct/?q=%w" },
    ]);
    public string? PDICPipeExecutablePath { get; set => Set(ref field, value); }
}

public enum SubAutoTextCopyTarget
{
    Primary,
    Secondary,
    All
}

public enum SubPositionAlignment
{
    Top,    // This is useful for dual subs because primary sub position is stayed
    Center, // Same as bitmap subs
    Bottom  // Normal video players use this
}

public class AppConfigTheme : Bindable
{
    private readonly PaletteHelper _paletteHelper = new();

    public Color PrimaryColor
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Theme cur = _paletteHelper.GetTheme();
                cur.SetPrimaryColor(value);
                _paletteHelper.SetTheme(cur);
            }
        }
    } = (Color)ColorConverter.ConvertFromString("#D23D6F"); // Pink
    // Desaturate and lighten from material pink 500
    // https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
    // $ pastel color E91E63 | pastel desaturate 0.2 | pastel lighten 0.015

    public Color SecondaryColor
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Theme cur = _paletteHelper.GetTheme();
                cur.SetSecondaryColor(value);
                _paletteHelper.SetTheme(cur);
            }
        }
    } = (Color)ColorConverter.ConvertFromString("#00B8D4"); // Cyan
}

public interface IMenuAction : INotifyPropertyChanged, ICloneable
{
    [JsonIgnore]
    string Type { get; }
    string Title { get; set; }
    bool IsEnabled { get; set; }
}

public class SearchMenuAction : Bindable, IMenuAction
{
    [JsonIgnore]
    public string Type => "Search";
    public required string Title { get; set; }
    public required string Url { get; set => Set(ref field, value); }
    public bool IsEnabled { get; set => Set(ref field, value); } = true;

    public object Clone()
    {
        return new SearchMenuAction
        {
            Title = Title,
            Url = Url,
            IsEnabled = IsEnabled
        };
    }
}

public class ClipboardMenuAction : Bindable, IMenuAction
{
    [JsonIgnore]
    public string Type => "Clipboard";
    public string Title { get; set; } = "Copy";
    public bool ToLower { get; set => Set(ref field, value); }
    public bool IsEnabled { get; set => Set(ref field, value); } = true;

    public object Clone()
    {
        return new ClipboardMenuAction
        {
            Title = Title,
            ToLower = ToLower,
            IsEnabled = IsEnabled
        };
    }
}

public class ClipboardAllMenuAction : Bindable, IMenuAction
{
    [JsonIgnore]
    public string Type => "ClipboardAll";
    public string Title { get; set; } = "Copy All";
    public bool ToLower { get; set => Set(ref field, value); }
    public bool IsEnabled { get; set => Set(ref field, value); } = true;

    public object Clone()
    {
        return new ClipboardAllMenuAction
        {
            Title = Title,
            ToLower = ToLower,
            IsEnabled = IsEnabled
        };
    }
}

public enum WordClickAction
{
    Translation,
    Clipboard,
    ClipboardAll,
    PDIC
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using FlyleafLib;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaPlayer;
using LLPlayer.Services;
using Microsoft.Xaml.Behaviors;

namespace LLPlayer.Controls;

public partial class FlyleafBar : UserControl
{
    public FlyleafManager FL { get; }

    public FlyleafBar()
    {
        InitializeComponent();

        FL = ((App)Application.Current).Container.Resolve<FlyleafManager>();

        DataContext = this;

        // Do not hide the cursor when it is on the seek bar
        MouseEnter += OnMouseEnter;
        LostFocus += OnMouseLeave;
        MouseLeave += OnMouseLeave;

        FL.Config.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == nameof(FL.Config.SeekBarShowOnlyMouseOver))
            {
                // Avoided a problem in which Opacity was set to 0 when switching settings and was not displayed.
                FL.Player.Activity.ForceFullActive();
                IsShowing = true;

                if (FL.Config.SeekBarShowOnlyMouseOver && !MyCard.IsMouseOver)
                {
                    IsShowing = false;
                }
            }
        };

        FL.Player.Activity.PropertyChanged += (sender, args) =>
        {
            switch (args.PropertyName)
            {
                case nameof(FL.Player.Activity.IsEnabled):
                    if (FL.Config.SeekBarShowOnlyMouseOver)
                    {
                        IsShowing = !FL.Player.Activity.IsEnabled || MyCard.IsMouseOver;
                    }
                    break;

                case nameof(FL.Player.Activity.Mode):
                    if (!FL.Config.SeekBarShowOnlyMouseOver)
                    {
                        IsShowing = FL.Player.Activity.Mode == ActivityMode.FullActive;
                    }
                    break;
            }
        };

        if (FL.Config.SeekBarShowOnlyMouseOver)
        {
            // start in hide
            MyCard.Opacity = 0.01;
        }
        else
        {
            // start in show
            IsShowing = true;
        }
    }

    public bool IsShowing
    {
        get;
        set
        {
            if (field == value)
                return;

            field = value;
            if (value)
            {
                // Fade In
                MyCard.BeginAnimation(OpacityProperty, new DoubleAnimation()
                {
                    BeginTime = TimeSpan.Zero,
                    To = 1,
                    Duration = new Duration(TimeSpan.FromMilliseconds(FL.Config.SeekBarFadeInTimeMs))
                });
            }
            else
            {
                // Fade Out
                MyCard.BeginAnimation(OpacityProperty, new DoubleAnimation()
                {
                    BeginTime = TimeSpan.Zero,
                    // TODO: L: needs to be almost transparent to receive MouseEnter events
                    To = FL.Config.SeekBarShowOnlyMouseOver ? 0.01 : 0,
                    Duration = new Duration(TimeSpan.FromMilliseconds(FL.Config.SeekBarFadeOutTimeMs))
                });
            }
        }
    }

    private void OnMouseLeave(object sender, RoutedEventArgs e)
    {
        if (FL.Config.SeekBarShowOnlyMouseOver && FL.Player.Activity.IsEnabled)
        {
            IsShowing = false;
        }
        else
        {
            SetActivity(true);
        }
    }

    private void OnMouseEnter(object sender, MouseEventArgs e)
    {
        if (FL.Config.SeekBarShowOnlyMouseOver && FL.Player.Activity.IsEnabled)
        {
            IsShowing = true;
        }
        else
        {
            SetActivity(false);
        }
    }

    private void SetActivity(bool isActive)
    {
        FL.Player.Activity.IsEnabled = isActive;
    }

    // Left-click on button to open context menu
    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        if (sender is not Button btn)
        {
            return;
        }

        if (btn.ContextMenu == null)
        {
            return;
        }

        if (btn.ContextMenu.PlacementTarget == null)
        {
            // Do not hide seek bar when context menu is displayed (register once)
            btn.ContextMenu.Opened += OnContextMenuOnOpened;
            btn.ContextMenu.Closed += OnContextMenuOnClosed;
            btn.ContextMenu.MouseMove += OnContextMenuOnMouseMove;
            btn.ContextMenu.PlacementTarget = btn;
        }
        btn.ContextMenu.IsOpen = true;
    }

    private void OnContextMenuOnOpened(object o, RoutedEventArgs args)
    {
        SetActivity(false);
    }

    private void OnContextMenuOnClosed(object o, RoutedEventArgs args)
    {
        SetActivity(true);
    }

    private void OnContextMenuOnMouseMove(object o, MouseEventArgs args)
    {
        // this is necessary to keep PopupMenu visible when opened in succession when SeekBarShowOnlyMouseOver
        SetActivity(false);
    }
}

public class SliderToolTipBehavior : Behavior<Slider>
{
    private const int PaddingSize = 5;
    private Popup? _valuePopup;
    private TextBlock? _tooltipText;
    private Border? _tooltipBorder;
    private Track? _track;

    public static readonly DependencyProperty ChaptersProperty =
        DependencyProperty.Register(nameof(Chapters), typeof(IList<Demuxer.Chapter>), typeof(SliderToolTipBehavior), new PropertyMetadata(null));

    public IList<Demuxer.Chapter>? Chapters
    {
        get => (IList<Demuxer.Chapter>)GetValue(ChaptersProperty);
        set => SetValue(ChaptersProperty, value);
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        _tooltipText = new TextBlock
        {
            Foreground = Brushes.White,
            FontSize = 12,
            TextAlignment = TextAlignment.Center
        };

        _tooltipBorder = new Border
        {
            Background = new SolidColorBrush(Colors.Black) { Opacity = 0.7 },
            CornerRadius = new CornerRadius(4),
            Padding = new Thickness(PaddingSize),
            Child = _tooltipText
        };

        _valuePopup = new Popup
        {
            AllowsTransparency = true,
            Placement = PlacementMode.Absolute,
            PlacementTarget = AssociatedObject,
            Child = _tooltipBorder
        };

        AssociatedObject.MouseMove += Slider_MouseMove;
        AssociatedObject.MouseLeave += Slider_MouseLeave;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        AssociatedObject.MouseMove -= Slider_MouseMove;
        AssociatedObject.MouseLeave -= Slider_MouseLeave;
    }

    private void Slider_MouseMove(object sender, MouseEventArgs e)
    {
        _track ??= (Track)AssociatedObject.Template.FindName("PART_Track", AssociatedObject);
        Point pos = e.GetPosition(_track);
        double value = _track.ValueFromPoint(pos);
        TimeSpan hoverTime = TimeSpan.FromSeconds(value);

        string dateFormat = hoverTime.Hours >= 1 ? @"hh\:mm\:ss" : @"mm\:ss";
        string timestamp = hoverTime.ToString(dateFormat);

        // TODO: L: Allow customizable chapter display functionality
        string? chapterTitle = GetChapterTitleAtTime(hoverTime);

        _tooltipText!.Inlines.Clear();
        if (chapterTitle != null)
        {
            _tooltipText.Inlines.Add(chapterTitle);
            _tooltipText.Inlines.Add(new LineBreak());
        }

        _tooltipText.Inlines.Add(timestamp);

        Window window = Window.GetWindow(AssociatedObject)!;
        Point cursorPoint = window.PointToScreen(e.GetPosition(window));
        Point sliderPoint = AssociatedObject.PointToScreen(default);

        // Fix for high dpi because PointToScreen returns physical coords
        cursorPoint.X /= Utils.NativeMethods.DpiX;
        cursorPoint.Y /= Utils.NativeMethods.DpiY;

        sliderPoint.X /= Utils.NativeMethods.DpiX;
        sliderPoint.Y /= Utils.NativeMethods.DpiY;

        // Display on top of slider near mouse
        _valuePopup!.HorizontalOffset = cursorPoint.X - (_tooltipText.ActualWidth + PaddingSize * 2) / 2;
        _valuePopup!.VerticalOffset = sliderPoint.Y - _tooltipBorder!.ActualHeight - 5;

        // display popup
        _valuePopup.IsOpen = true;
    }

    private void Slider_MouseLeave(object sender, MouseEventArgs e)
    {
        _valuePopup!.IsOpen = false;
    }

    private string? GetChapterTitleAtTime(TimeSpan time)
    {
        if (Chapters == null || Chapters.Count <= 1)
        {
            return null;
        }

        var chapter = Chapters.FirstOrDefault(c => c.StartTime <= time.Ticks && time.Ticks <= c.EndTime);
        return chapter?.Title;
    }
}

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;

namespace LLPlayer.Controls;

/// <summary>
/// Popup with code to not be the topmost control
/// ref: https://gist.github.com/flq/903202/8bf56606b7c6fad341c31482f196b3aba9373e07
/// </summary>
public class NonTopmostPopup : Popup
{
    private bool? _appliedTopMost;
    private bool _alreadyLoaded;
    private Window? _parentWindow;

    public static readonly DependencyProperty IsTopmostProperty =
        DependencyProperty.Register(nameof(IsTopmost), typeof(bool), typeof(NonTopmostPopup), new FrameworkPropertyMetadata(false, OnIsTopmostChanged));

    public bool IsTopmost
    {
        get => (bool)GetValue(IsTopmostProperty);
        set => SetValue(IsTopmostProperty, value);
    }

    public NonTopmostPopup()
    {
        Loaded += OnPopupLoaded;
    }

    void OnPopupLoaded(object sender, RoutedEventArgs e)
    {
        if (_alreadyLoaded)
            return;

        _alreadyLoaded = true;

        if (Child != null)
        {
            Child.AddHandler(PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnChildPreviewMouseLeftButtonDown), true);
        }

        //_parentWindow = Window.GetWindow(this);
        _parentWindow = Application.Current.MainWindow;

        if (_parentWindow == null)
            return;

        _parentWindow.Activated += OnParentWindowActivated;
        _parentWindow.Deactivated += OnParentWindowDeactivated;
        _parentWindow.LocationChanged += OnParentWindowLocationChanged;
    }

    private void OnParentWindowLocationChanged(object? sender, EventArgs e)
    {
        try
        {
            var method = typeof(Popup).GetMethod("UpdatePosition",
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (IsOpen)
            {
                method?.Invoke(this, null);
            }
        }
        catch
        {
            // ignored
        }
    }

    private void OnParentWindowActivated(object? sender, EventArgs e)
    {
        //Debug.WriteLine("Parent Window Activated");
        SetTopmostState(true);
    }

    private void OnParentWindowDeactivated(object? sender, EventArgs e)
    {
        //Debug.WriteLine("Parent Window Deactivated");

        if (IsTopmost == false)
        {
            SetTopmostState(IsTopmost);
        }
    }

    private void OnChildPreviewMouseLeftButtonDown(object? sender, MouseButtonEventArgs e)
    {
        //Debug.WriteLine("Child Mouse Left Button Down");

        SetTopmostState(true);

        if (_parentWindow != null && !_parentWindow.IsActive && IsTopmost == false)
        {
            _parentWindow.Activate();
            //Debug.WriteLine("Activating Parent from child Left Button Down");
        }
    }

    private static void OnIsTopmostChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var thisobj = (NonTopmostPopup)obj;

        thisobj.SetTopmostState(thisobj.IsTopmost);
    }

    protected override void OnOpened(EventArgs e)
    {
        SetTopmostState(IsTopmost);
    }

    private void SetTopmostState(bool isTop)
    {
        // Don't apply state if it's the same as incoming state
        if (_appliedTopMost.HasValue && _appliedTopMost == isTop)
        {
            // TODO: L: Maybe commenting out this section will solve the problem of the rare popups coming to the forefront?
            //return;
        }

        if (Child == null)
            return;

        var hwndSource = (PresentationSource.FromVisual(Child)) as HwndSource;

        if (hwndSource == null)
            return;
        var hwnd = hwndSource.Handle;

        if (!GetWindowRect(hwnd, out RECT rect))
            return;

        //Debug.WriteLine("setting z-order " + isTop);

        if (isTop)
        {
            SetWindowPos(hwnd, HWND_TOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
        }
        else
        {
            // Z-Order would only get refreshed/reflected if clicking the
            // the titlebar (as opposed to other parts of the external
            // window) unless I first set the popup to HWND_BOTTOM
            // then HWND_TOP before HWND_NOTOPMOST
            SetWindowPos(hwnd, HWND_BOTTOM, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
            SetWindowPos(hwnd, HWND_TOP, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
            SetWindowPos(hwnd, HWND_NOTOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
        }

        _appliedTopMost = isTop;
    }

    #region P/Invoke imports & definitions
#pragma warning disable 1591 //Xml-doc
#pragma warning disable 169 //Never used-warning
    // ReSharper disable InconsistentNaming
    // Imports etc. with their naming rules

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT

    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X,
    int Y, int cx, int cy, uint uFlags);

    static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
    static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
    static readonly IntPtr HWND_TOP = new IntPtr(0);
    static readonly IntPtr HWND_BOTTOM = new IntPtr(1);

    private const UInt32 SWP_NOSIZE = 0x0001;
    const UInt32 SWP_NOMOVE = 0x0002;
    //const UInt32 SWP_NOZORDER = 0x0004;
    const UInt32 SWP_NOREDRAW = 0x0008;
    const UInt32 SWP_NOACTIVATE = 0x0010;

    //const UInt32 SWP_FRAMECHANGED = 0x0020; /* The frame changed: send WM_NCCALCSIZE */
    //const UInt32 SWP_SHOWWINDOW = 0x0040;
    //const UInt32 SWP_HIDEWINDOW = 0x0080;
    //const UInt32 SWP_NOCOPYBITS = 0x0100;
    const UInt32 SWP_NOOWNERZORDER = 0x0200; /* Don't do owner Z ordering */
    const UInt32 SWP_NOSENDCHANGING = 0x0400; /* Don't send WM_WINDOWPOSCHANGING */

    const UInt32 TOPMOST_FLAGS =
          SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING;

    // ReSharper restore InconsistentNaming
#pragma warning restore 1591
#pragma warning restore 169
    #endregion
}

using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace FlyleafLib;

#pragma warning disable CA1401 // P/Invokes should not be visible
public static partial class Utils
{
    public static class NativeMethods
    {
        static NativeMethods()
        {
            if (IntPtr.Size == 4)
            {
                GetWindowLong = GetWindowLongPtr32;
                SetWindowLong = SetWindowLongPtr32;
            }
            else
            {
                GetWindowLong = GetWindowLongPtr64;
                SetWindowLong = SetWindowLongPtr64;
            }

            GetDPI(out DpiX, out DpiY);
        }

        public static Func<IntPtr, int, IntPtr, IntPtr> SetWindowLong;
        public static Func<IntPtr, int, IntPtr> GetWindowLong;

        [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
        public static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
        public static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
        public static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] // , SetLastError = true
        public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        [DllImport("user32.dll")]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("comctl32.dll")]
        public static extern bool SetWindowSubclass(IntPtr hWnd, IntPtr pfnSubclass, UIntPtr uIdSubclass, UIntPtr dwRefData);

        [DllImport("comctl32.dll")]
        public static extern bool RemoveWindowSubclass(IntPtr hWnd, IntPtr pfnSubclass, UIntPtr uIdSubclass);

        [DllImport("comctl32.dll")]
        public static extern IntPtr DefSubclassProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, UInt32 uFlags);

        [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
        public static extern int StrCmpLogicalW(string psz1, string psz2);

        [DllImport("user32.dll")]
        public static extern int ShowCursor(bool bShow);

        [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
        public static extern uint TimeBeginPeriod(uint uMilliseconds);

        [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
        public static extern uint TimeEndPeriod(uint uMilliseconds);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
        [FlagsAttribute]
        public enum EXECUTION_STATE :uint
        {
            ES_AWAYMODE_REQUIRED    = 0x00000040,
            ES_CONTINUOUS           = 0x80000000,
            ES_DISPLAY_REQUIRED     = 0x00000002,
            ES_SYSTEM_REQUIRED      = 0x00000001
        }

        [DllImport("gdi32.dll")]
        public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

        [DllImport("User32.dll")]
        public static extern int GetWindowRgn(IntPtr hWnd, IntPtr hRgn);

        [DllImport("User32.dll")]
        public static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

        [DllImport("user32.dll")]
        public static extern bool GetWindowRect(IntPtr hwnd, ref RECT rectangle);

        [DllImport("user32.dll")]
        public static extern bool GetWindowInfo(IntPtr hwnd, ref WINDOWINFO pwi);

        [DllImport("user32.dll")]
        public static extern void SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

        [DllImport("user32.dll")]
        public static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        [StructLayout(LayoutKind.Sequential)]
        public struct WINDOWINFO
        {
            public uint cbSize;
            public RECT rcWindow;
            public RECT rcClient;
            public uint dwStyle;
            public uint dwExStyle;
            public uint dwWindowStatus;
            public uint cxWindowBorders;
            public uint cyWindowBorders;
            public ushort atomWindowType;
            public ushort wCreatorVersion;

            // Allows automatic initialization of "cbSize" with "new WINDOWINFO(null/true/false)".
            public WINDOWINFO(Boolean? filler) : this()
                => cbSize = (UInt32)Marshal.SizeOf(typeof(WINDOWINFO));

        }
        public struct RECT
        {
            public int Left     { get; set; }
            public int Top      { get; set; }
            public int Right    { get; set; }
            public int Bottom   { get; set; }
        }

        [Flags]
        public enum SetWindowPosFlags : uint
        {
            SWP_ASYNCWINDOWPOS = 0x4000,
            SWP_DEFERERASE = 0x2000,
            SWP_DRAWFRAME = 0x0020,
            SWP_FRAMECHANGED = 0x0020,
            SWP_HIDEWINDOW = 0x0080,
            SWP_NOACTIVATE = 0x0010,
            SWP_NOCOPYBITS = 0x0100,
            SWP_NOMOVE = 0x0002,
            SWP_NOOWNERZORDER = 0x0200,
            SWP_NOREDRAW = 0x0008,
            SWP_NOREPOSITION = 0x0200,
            SWP_NOSENDCHANGING = 0x0400,
            SWP_NOSIZE = 0x0001,
            SWP_NOZORDER = 0x0004,
            SWP_SHOWWINDOW = 0x0040,
        }

        [Flags]
        public enum WindowLongFlags : int
        {
             GWL_EXSTYLE = -20,
             GWLP_HINSTANCE = -6,
             GWLP_HWNDPARENT = -8,
             GWL_ID = -12,
             GWL_STYLE = -16,
             GWL_USERDATA = -21,
             GWL_WNDPROC = -4,
             DWLP_USER = 0x8,
             DWLP_MSGRESULT = 0x0,
             DWLP_DLGPROC = 0x4
        }

        [Flags]
        public enum WindowStyles : uint
        {
            WS_BORDER = 0x800000,
            WS_CAPTION = 0xc00000,
            WS_CHILD = 0x40000000,
            WS_CLIPCHILDREN = 0x2000000,
            WS_CLIPSIBLINGS = 0x4000000,
            WS_DISABLED = 0x8000000,
            WS_DLGFRAME = 0x400000,
            WS_GROUP = 0x20000,
            WS_HSCROLL = 0x100000,
            WS_MAXIMIZE = 0x1000000,
            WS_MAXIMIZEBOX = 0x10000,
            WS_MINIMIZE = 0x20000000,
            WS_MINIMIZEBOX = 0x20000,
            WS_OVERLAPPED = 0x0,
            WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_SIZEFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
            WS_POPUP = 0x80000000,
            WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU,
            WS_SIZEFRAME = 0x40000,
            WS_SYSMENU = 0x80000,
            WS_TABSTOP = 0x10000,
            WS_THICKFRAME = 0x00040000,
            WS_VISIBLE = 0x10000000,
            WS_VSCROLL = 0x200000
        }

        [Flags]
        public enum WindowStylesEx : uint
        {
            WS_EX_ACCEPTFILES = 0x00000010,
            WS_EX_APPWINDOW = 0x00040000,
            WS_EX_CLIENTEDGE = 0x00000200,
            WS_EX_COMPOSITED = 0x02000000,
            WS_EX_CONTEXTHELP = 0x00000400,
            WS_EX_CONTROLPARENT = 0x00010000,
            WS_EX_DLGMODALFRAME = 0x00000001,
            WS_EX_LAYERED = 0x00080000,
            WS_EX_LAYOUTRTL = 0x00400000,
            WS_EX_LEFT = 0x00000000,
            WS_EX_LEFTSCROLLBAR = 0x00004000,
            WS_EX_LTRREADING = 0x00000000,
            WS_EX_MDICHILD = 0x00000040,
            WS_EX_NOACTIVATE = 0x08000000,
            WS_EX_NOINHERITLAYOUT = 0x00100000,
            WS_EX_NOPARENTNOTIFY = 0x00000004,
            WS_EX_NOREDIRECTIONBITMAP = 0x00200000,
            WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE,
            WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
            WS_EX_RIGHT = 0x00001000,
            WS_EX_RIGHTSCROLLBAR = 0x00000000,
            WS_EX_RTLREADING = 0x00002000,
            WS_EX_STATICEDGE = 0x00020000,
            WS_EX_TOOLWINDOW = 0x00000080,
            WS_EX_TOPMOST = 0x00000008,
            WS_EX_TRANSPARENT = 0x00000020,
            WS_EX_WINDOWEDGE = 0x00000100
        }

        public enum ShowWindowCommands : uint
        {
            SW_HIDE = 0,
            SW_SHOWNORMAL = 1,
            SW_NORMAL = 1,
            SW_SHOWMINIMIZED = 2,
            SW_SHOWMAXIMIZED = 3,
            SW_MAXIMIZE = 3,
            SW_SHOWNOACTIVATE = 4,
            SW_SHOW = 5,
            SW_MINIMIZE = 6,
            SW_SHOWMINNOACTIVE = 7,
            SW_SHOWNA = 8,
            SW_RESTORE = 9,
            SW_SHOWDEFAULT = 10,
            SW_FORCEMINIMIZE = 11,
            SW_MAX = 11
        }

        public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
        public delegate IntPtr SubclassWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData);

        public static int SignedHIWORD(IntPtr n) => SignedHIWORD(unchecked((int)(long)n));
        public static int SignedLOWORD(IntPtr n) => SignedLOWORD(unchecked((int)(long)n));
        public static int SignedHIWORD(int n) => (short)((n >> 16) & 0xffff);
        public static int SignedLOWORD(int n) => (short)(n & 0xFFFF);

        #region DPI
        public static double DpiX, DpiY;
        public static int DpiXSource, DpiYSource;
        const int LOGPIXELSX = 88, LOGPIXELSY = 90;
        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
        public static extern int GetDeviceCaps(IntPtr hDC, int nIndex);
        public static void GetDPI(out double dpiX, out double dpiY) => GetDPI(IntPtr.Zero, out dpiX, out dpiY);
        public static void GetDPI(IntPtr handle, out double dpiX, out double dpiY)
        {
            Graphics GraphicsObject = Graphics.FromHwnd(handle); // DESKTOP Handle
            IntPtr dcHandle = GraphicsObject.GetHdc();
            DpiXSource = GetDeviceCaps(dcHandle, LOGPIXELSX);
            dpiX = DpiXSource / 96.0;
            DpiYSource = GetDeviceCaps(dcHandle, LOGPIXELSY);
            dpiY = DpiYSource / 96.0;
            GraphicsObject.ReleaseHdc(dcHandle);
            GraphicsObject.Dispose();
        }
        #endregion
    }
}
#pragma warning restore CA1401 // P/Invokes should not be visible

using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Media;

namespace LLPlayer.Controls;

[ContentProperty("Text")]
public class OutlinedTextBlock : FrameworkElement
{
    public OutlinedTextBlock()
    {
        UpdatePen();
        TextDecorations = new TextDecorationCollection();
    }

    #region dependency properties
    public static readonly DependencyProperty FillProperty = DependencyProperty.Register(
            nameof(Fill),
            typeof(Brush),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(Brushes.White, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(
            nameof(Stroke),
            typeof(Brush),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(
            nameof(StrokeThickness),
            typeof(double),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontSizeInitialProperty = DependencyProperty.Register(
            nameof(FontSizeInitial),
            typeof(double),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontSizeAutoProperty = DependencyProperty.Register(
        nameof(FontSizeAuto),
        typeof(bool),
        typeof(OutlinedTextBlock),
        new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty StrokePositionProperty = DependencyProperty.Register(
            nameof(StrokePosition),
            typeof(StrokePosition),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(StrokePosition.Outside, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty StrokeThicknessInitialProperty = DependencyProperty.Register(
            nameof(StrokeThicknessInitial),
            typeof(double),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            nameof(Text),
            typeof(string),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextInvalidated));

    public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register(
            nameof(TextAlignment),
            typeof(TextAlignment),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register(
            nameof(TextDecorations),
            typeof(TextDecorationCollection),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty TextTrimmingProperty = DependencyProperty.Register(
            nameof(TextTrimming),
            typeof(TextTrimming),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(OnFormattedTextUpdated));

    public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register(
            nameof(TextWrapping),
            typeof(TextWrapping),
            typeof(OutlinedTextBlock),
            new FrameworkPropertyMetadata(TextWrapping.NoWrap, OnFormattedTextUpdated));

    private FormattedText? _FormattedText;
    private Geometry? _TextGeometry;
    private Pen? _Pen;
    private PathGeometry? _clipGeometry;

    public Brush Fill
    {
        get => (Brush)GetValue(FillProperty);
        set => SetValue(FillProperty, value);
    }

    public FontFamily FontFamily
    {
        get => (FontFamily)GetValue(FontFamilyProperty);
        set => SetValue(FontFamilyProperty, value);
    }

    [TypeConverter(typeof(FontSizeConverter))]
    public double FontSize
    {
        get => (double)GetValue(FontSizeProperty);
        set => SetValue(FontSizeProperty, value);
    }

    [TypeConverter(typeof(FontSizeConverter))]
    public double FontSizeInitial
    {
        get => (double)GetValue(FontSizeInitialProperty);
        set => SetValue(FontSizeInitialProperty, value);
    }

    public bool FontSizeAuto
    {
        get => (bool)GetValue(FontSizeAutoProperty);
        set => SetValue(FontSizeAutoProperty, value);
    }

    public FontStretch FontStretch
    {
        get => (FontStretch)GetValue(FontStretchProperty);
        set => SetValue(FontStretchProperty, value);
    }

    public FontStyle FontStyle
    {
        get => (FontStyle)GetValue(FontStyleProperty);
        set => SetValue(FontStyleProperty, value);
    }

    public FontWeight FontWeight
    {
        get => (FontWeight)GetValue(FontWeightProperty);
        set => SetValue(FontWeightProperty, value);
    }

    public Brush Stroke
    {
        get => (Brush)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }

    public StrokePosition StrokePosition
    {
        get => (StrokePosition)GetValue(StrokePositionProperty);
        set => SetValue(StrokePositionProperty, value);
    }

    public double StrokeThickness
    {
        get => (double)GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }

    public double StrokeThicknessInitial
    {
        get => (double)GetValue(StrokeThicknessInitialProperty);
        set => SetValue(StrokeThicknessInitialProperty, value);
    }

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public TextAlignment TextAlignment
    {
        get => (TextAlignment)GetValue(TextAlignmentProperty);
        set => SetValue(TextAlignmentProperty, value);
    }

    public TextDecorationCollection TextDecorations
    {
        get => (TextDecorationCollection)GetValue(TextDecorationsProperty);
        set => SetValue(TextDecorationsProperty, value);
    }

    public TextTrimming TextTrimming
    {
        get => (TextTrimming)GetValue(TextTrimmingProperty);
        set => SetValue(TextTrimmingProperty, value);
    }

    public TextWrapping TextWrapping
    {
        get => (TextWrapping)GetValue(TextWrappingProperty);
        set => SetValue(TextWrappingProperty, value);
    }

    #endregion

    #region PDIC

    public int WordOffset { get; set; }

    #endregion

    private void UpdatePen()
    {
        _Pen = new Pen(Stroke, StrokeThickness)
        {
            DashCap = PenLineCap.Round,
            EndLineCap = PenLineCap.Round,
            LineJoin = PenLineJoin.Round,
            StartLineCap = PenLineCap.Round
        };

        if (StrokePosition == StrokePosition.Outside || StrokePosition == StrokePosition.Inside)
            _Pen.Thickness = StrokeThickness * 2;

        InvalidateVisual();
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        EnsureGeometry();

        drawingContext.DrawGeometry(Fill, null, _TextGeometry);

        if (StrokePosition == StrokePosition.Outside)
            drawingContext.PushClip(_clipGeometry);
        else if (StrokePosition == StrokePosition.Inside)
            drawingContext.PushClip(_TextGeometry);

        drawingContext.DrawGeometry(null, _Pen, _TextGeometry);

        if (StrokePosition == StrokePosition.Outside || StrokePosition == StrokePosition.Inside)
            drawingContext.Pop();
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        EnsureFormattedText();

        // constrain the formatted text according to the available size
        double w = availableSize.Width;
        double h = availableSize.Height;

        if (FontSizeAuto)
        {
            if (FontSizeInitial > 0)
            {
                double r = w / 1920; // FontSizeInitial should be based on fixed Screen Width (eg. Full HD 1920)
                FontSize = FontSizeInitial * (r + ((1 - r) * 0.20f)); // TBR: Weight/Percentage for how much it will be affected by the change (possible dependency property / config)
                _FormattedText!.SetFontSize(FontSize);
            }
        }

        if (StrokeThicknessInitial > 0)
        {
            double r = FontSize / 48; // StrokeThicknessInitial should be based on fixed FontSize (eg. 48)
            StrokeThickness = Math.Max(1, StrokeThicknessInitial * r);
            UpdatePen();
        }

        // the Math.Min call is important - without this constraint (which seems arbitrary, but is the maximum allowable text width), things blow up when availableSize is infinite in both directions
        // the Math.Max call is to ensure we don't hit zero, which will cause MaxTextHeight to throw
        _FormattedText!.MaxTextWidth = Math.Min(3579139, w);
        _FormattedText!.MaxTextHeight = Math.Max(0.0001d, h);

        // return the desired size
        return new Size(Math.Ceiling(_FormattedText.Width), Math.Ceiling(_FormattedText.Height));
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        EnsureFormattedText();

        // update the formatted text with the final size
        _FormattedText!.MaxTextWidth = finalSize.Width;
        _FormattedText!.MaxTextHeight = Math.Max(0.0001d, finalSize.Height);

        // need to re-generate the geometry now that the dimensions have changed
        _TextGeometry = null;
        UpdatePen();

        return finalSize;
    }

    private static void OnFormattedTextInvalidated(DependencyObject dependencyObject,
      DependencyPropertyChangedEventArgs e)
    {
        var outlinedTextBlock = (OutlinedTextBlock)dependencyObject;
        outlinedTextBlock._FormattedText = null;
        outlinedTextBlock._TextGeometry = null;

        outlinedTextBlock.InvalidateMeasure();
        outlinedTextBlock.InvalidateVisual();
    }

    private static void OnFormattedTextUpdated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var outlinedTextBlock = (OutlinedTextBlock)dependencyObject;
        if (outlinedTextBlock._FormattedText != null)
            outlinedTextBlock.UpdateFormattedText();
        outlinedTextBlock._TextGeometry = null;

        outlinedTextBlock.InvalidateMeasure();
        outlinedTextBlock.InvalidateVisual();
    }

    private void EnsureFormattedText()
    {
        if (_FormattedText != null)
            return;

        _FormattedText = new FormattedText(
          Text ?? "",
          CultureInfo.CurrentUICulture,
          FlowDirection,
          new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
          FontSize,
          Brushes.Black,
          VisualTreeHelper.GetDpi(this).PixelsPerDip);

        UpdateFormattedText();
    }

    private void UpdateFormattedText()
    {
        _FormattedText!.MaxLineCount = TextWrapping == TextWrapping.NoWrap ? 1 : int.MaxValue;
        _FormattedText.TextAlignment = TextAlignment;
        _FormattedText.Trimming = TextTrimming;
        _FormattedText.SetFontSize(FontSize);
        _FormattedText.SetFontStyle(FontStyle);
        _FormattedText.SetFontWeight(FontWeight);
        _FormattedText.SetFontFamily(FontFamily);
        _FormattedText.SetFontStretch(FontStretch);
        _FormattedText.SetTextDecorations(TextDecorations);
    }

    private void EnsureGeometry()
    {
        if (_TextGeometry != null)
            return;

        EnsureFormattedText();
        _TextGeometry = _FormattedText!.BuildGeometry(new Point(0, 0));

        if (StrokePosition == StrokePosition.Outside)
        {
            var boundsGeo = new RectangleGeometry(new Rect(-(2 * StrokeThickness),
                    -(2 * StrokeThickness), ActualWidth + (4 * StrokeThickness), ActualHeight + (4 * StrokeThickness)));
            _clipGeometry = Geometry.Combine(boundsGeo, _TextGeometry, GeometryCombineMode.Exclude, null);
        }
    }
}

public enum StrokePosition
{
    Center,
    Outside,
    Inside
}

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CliWrap;
using Microsoft.Win32;

namespace FlyleafLib;

public static partial class Utils
{
    // VLC : https://github.com/videolan/vlc/blob/master/modules/gui/qt/dialogs/preferences/simple_preferences.cpp
    // Kodi: https://github.com/xbmc/xbmc/blob/master/xbmc/settings/AdvancedSettings.cpp

    public static List<string> ExtensionsAudio = new()
    {
        // VLC
          "3ga" , "669" , "a52" , "aac" , "ac3"
        , "adt" , "adts", "aif" , "aifc", "aiff"
        , "au"  , "amr" , "aob" , "ape" , "caf"
        , "cda" , "dts" , "flac", "it"  , "m4a"
        , "m4p" , "mid" , "mka" , "mlp" , "mod"
        , "mp1" , "mp2" , "mp3" , "mpc" , "mpga"
        , "oga" , "oma" , "opus", "qcp" , "ra"
        , "rmi" , "snd" , "s3m" , "spx" , "tta"
        , "voc" , "vqf" , "w64" , "wav" , "wma"
        , "wv"  , "xa"  , "xm"
    };

    public static List<string> ExtensionsPictures = new()
    {
        "apng", "bmp", "gif", "jpg", "jpeg", "png", "ico", "tif", "tiff", "tga","jfif"
    };

    public static List<string> ExtensionsSubtitlesText = new()
    {
        "ass", "ssa", "srt", "txt", "text", "vtt"
    };

    public static List<string> ExtensionsSubtitlesBitmap = new()
    {
        "sub", "sup"
    };

    public static List<string> ExtensionsSubtitles = [..ExtensionsSubtitlesText, ..ExtensionsSubtitlesBitmap];

    public static List<string> ExtensionsVideo = new()
    {
        // VLC
          "3g2" , "3gp" , "3gp2", "3gpp", "amrec"
        , "amv" , "asf" , "avi" , "bik" , "divx"
        , "drc" , "dv"  , "f4v" , "flv" , "gvi"
        , "gxf" , "m1v" , "m2t" , "m2v" , "m2ts"
        , "m4v" , "mkv" , "mov" , "mp2v", "mp4"
        , "mp4v", "mpa" , "mpe" , "mpeg", "mpeg1"
        , "mpeg2","mpeg4","mpg" , "mpv2", "mts"
        , "mtv" , "mxf" , "nsv" , "nuv" , "ogg"
        , "ogm" , "ogx" , "ogv" , "rec" , "rm"
        , "rmvb", "rpl" , "thp" , "tod" , "ts"
        , "tts" , "vob" , "vro" , "webm", "wmv"
        , "xesc"

        // Additional
        , "dav"
    };

    private static int uniqueId;
    public static int GetUniqueId() { Interlocked.Increment(ref uniqueId); return uniqueId; }

    /// <summary>
    /// Begin Invokes the UI thread to execute the specified action
    /// </summary>
    /// <param name="action"></param>
    public static void UI(Action action)
    {
#if DEBUG
        if (Application.Current == null)
            return;
#endif

        Application.Current.Dispatcher.BeginInvoke(action, System.Windows.Threading.DispatcherPriority.DataBind);
    }

    /// <summary>
    /// Begin Invokes the UI thread if required to execute the specified action
    /// </summary>
    /// <param name="action"></param>
    public static void UIIfRequired(Action action)
    {
        if (Thread.CurrentThread.ManagedThreadId == Application.Current.Dispatcher.Thread.ManagedThreadId)
            action();
        else
            Application.Current.Dispatcher.BeginInvoke(action);
    }

    /// <summary>
    /// Invokes the UI thread to execute the specified action
    /// </summary>
    /// <param name="action"></param>
    public static void UIInvoke(Action action) => Application.Current.Dispatcher.Invoke(action);

    /// <summary>
    /// Invokes the UI thread if required to execute the specified action
    /// </summary>
    /// <param name="action"></param>
    public static void UIInvokeIfRequired(Action action)
    {
        if (Thread.CurrentThread.ManagedThreadId == Application.Current.Dispatcher.Thread.ManagedThreadId)
            action();
        else
            Application.Current.Dispatcher.Invoke(action);
    }

    public static Thread STA(Action action)
    {
        Thread thread = new(() => action());
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return thread;
    }

    public static void STAInvoke(Action action)
    {
        Thread thread = STA(action);
        thread.Join();
    }

    public static int Align(int num, int align)
    {
        int mod = num % align;
        return mod == 0 ? num : num + (align - mod);
    }
    public static float Scale(float value, float inMin, float inMax, float outMin, float outMax)
        => ((value - inMin) * (outMax - outMin) / (inMax - inMin)) + outMin;

    /// <summary>
    /// Adds a windows firewall rule if not already exists for the specified program path
    /// </summary>
    /// <param name="ruleName">Default value is Flyleaf</param>
    /// <param name="path">Default value is current executable path</param>
    public static void AddFirewallRule(string ruleName = null, string path = null)
    {
        Task.Run(() =>
        {
            try
            {
                if (string.IsNullOrEmpty(ruleName))
                    ruleName = "Flyleaf";

                if (string.IsNullOrEmpty(path))
                    path = Process.GetCurrentProcess().MainModule.FileName;

                path = $"\"{path}\"";

                // Check if rule already exists
                Process proc = new()
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "cmd",
                        Arguments = $"/C netsh advfirewall firewall show rule name={ruleName} verbose | findstr /L {path}",
                        CreateNoWindow = true,
                        UseShellExecute = false,
                        RedirectStandardOutput
                                        = true,
                        WindowStyle = ProcessWindowStyle.Hidden
                    }
                };

                proc.Start();
                proc.WaitForExit();

                if (proc.StandardOutput.Read() > 0)
                    return;

                // Add rule with admin rights
                proc = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "cmd",
                        Arguments = $"/C netsh advfirewall firewall add rule name={ruleName} dir=in  action=allow enable=yes program={path} profile=any &" +
                                             $"netsh advfirewall firewall add rule name={ruleName} dir=out action=allow enable=yes program={path} profile=any",
                        Verb = "runas",
                        CreateNoWindow = true,
                        UseShellExecute = true,
                        WindowStyle = ProcessWindowStyle.Hidden
                    }
                };

                proc.Start();
                proc.WaitForExit();

                Log($"Firewall rule \"{ruleName}\" added for {path}");
            }
            catch { }
        });
    }

    // We can't trust those
    //public static private bool    IsDesignMode=> (bool) DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue;
    //public static bool            IsDesignMode    = LicenseManager.UsageMode == LicenseUsageMode.Designtime; // Will not work properly (need to be called from non-static class constructor)

    //public static bool          IsWin11         = Regex.IsMatch(Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ProductName", "").ToString(), "Windows 11");
    //public static bool          IsWin10         = Regex.IsMatch(Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ProductName", "").ToString(), "Windows 10");
    //public static bool          IsWin8          = Regex.IsMatch(Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ProductName", "").ToString(), "Windows 8");
    //public static bool          IsWin7          = Regex.IsMatch(Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ProductName", "").ToString(), "Windows 7");

    public static List<string> GetMoviesSorted(List<string> movies)
    {
        List<string> moviesSorted = new();

        for (int i = 0; i < movies.Count; i++)
        {
            string ext = Path.GetExtension(movies[i]);

            if (ext == null || ext.Trim() == "")
                continue;

            if (ExtensionsVideo.Contains(ext[1..].ToLower()))
                moviesSorted.Add(movies[i]);
        }

        moviesSorted.Sort(new NaturalStringComparer());

        return moviesSorted;
    }
    public sealed class NaturalStringComparer : IComparer<string>
        { public int Compare(string a, string b) => NativeMethods.StrCmpLogicalW(a, b); }

    public static string GetRecInnerException(Exception e)
    {
        string dump = "";
        var cur = e.InnerException;

        for (int i = 0; i < 4; i++)
        {
            if (cur == null) break;
            dump += "\r\n - " + cur.Message;
            cur = cur.InnerException;
        }

        return dump;
    }
    public static string GetUrlExtention(string url)
    {
        int index;
        if ((index = url.LastIndexOf('.')) > 0)
            return url[(index + 1)..].ToLower();

        return "";
    }

    public static List<Language> GetSystemLanguages()
    {
        List<Language> Languages = [ Language.English ];

        if (OriginalCulture.ThreeLetterISOLanguageName != "eng")
            Languages.Add(Language.Get(OriginalCulture));

        foreach (System.Windows.Forms.InputLanguage lang in System.Windows.Forms.InputLanguage.InstalledInputLanguages)
            if (lang.Culture.ThreeLetterISOLanguageName != OriginalCulture.ThreeLetterISOLanguageName && lang.Culture.ThreeLetterISOLanguageName != "eng")
                Languages.Add(Language.Get(lang.Culture));

        return Languages;
    }

    public static CultureInfo OriginalCulture { get; private set; }
    public static CultureInfo OriginalUICulture { get; private set; }

    public static void SaveOriginalCulture()
    {
        OriginalCulture = CultureInfo.CurrentCulture;
        OriginalUICulture = CultureInfo.CurrentUICulture;
    }

    public class MediaParts
    {
        public string   Title       { get; set; } = "";
        public string   Extension   { get; set; } = "";
        public int      Season      { get; set; }
        public int      Episode     { get; set; }
        public int      Year        { get; set; }
    }
    public static MediaParts GetMediaParts(string title, bool checkSeasonEpisodeOnly = false)
    {
        Match res;
        MediaParts mp = new();
        int index = int.MaxValue; // title end pos

        res = RxSeasonEpisode1().Match(title);
        if (!res.Success)
        {
            res = RxSeasonEpisode2().Match(title);

            if (!res.Success)
                res = RxEpisodePart().Match(title);
        }

        if (res.Groups.Count > 1)
        {
            if (res.Groups["season"].Value != "")
                mp.Season = int.Parse(res.Groups["season"].Value);

            if (res.Groups["episode"].Value != "")
                mp.Episode = int.Parse(res.Groups["episode"].Value);

            if (checkSeasonEpisodeOnly || res.Index == 0) // 0: No title just season/episode
                return mp;

            index = res.Index;
        }

        mp.Extension = GetUrlExtention(title);
        if (mp.Extension.Length > 0 && mp.Extension.Length < 5)
            title = title[..(title.Length - mp.Extension.Length - 1)];

        // non-movie words, 1080p, 2015
        if ((res = RxExtended().Match(title)).Index > 0 && res.Index < index)
            index = res.Index;

        if ((res = RxDirectorsCut().Match(title)).Index > 0 && res.Index < index)
            index = res.Index;

        if ((res = RxBrrip().Match(title)).Index > 0 && res.Index < index)
            index = res.Index;

        if ((res = RxResolution().Match(title)).Index > 0 && res.Index < index)
            index = res.Index;

        res = RxYear().Match(title);
        Group gc;
        if (res.Success && (gc = res.Groups["year"]).Index > 2)
        {
            mp.Year = int.Parse(gc.Value);
            if (res.Index < index)
                index = res.Index;
        }

        if (index != int.MaxValue)
            title = title[..index];

        title = title.Replace(".", " ").Replace("_", " ");
        title = RxSpaces().Replace(title, " ");
        title = RxNonAlphaNumeric().Replace(title, "");

        mp.Title = title.Trim();

        return mp;
    }

    public static string FindNextAvailableFile(string fileName)
    {
        if (!File.Exists(fileName)) return fileName;

        string tmp = Path.Combine(Path.GetDirectoryName(fileName), Regex.Replace(Path.GetFileNameWithoutExtension(fileName), @"(.*) (\([0-9]+)\)$", "$1"));
        string newName;

        for (int i = 1; i < 101; i++)
        {
            newName = tmp + " (" + i + ")" + Path.GetExtension(fileName);
            if (!File.Exists(newName)) return newName;
        }

        return null;
    }
    public static string GetValidFileName(string name) => string.Join("_", name.Split(Path.GetInvalidFileNameChars()));

    public static string FindFileBelow(string filename)
    {
        string current = AppDomain.CurrentDomain.BaseDirectory;

        while (current != null)
        {
            if (File.Exists(Path.Combine(current, filename)))
                return Path.Combine(current, filename);

            current = Directory.GetParent(current)?.FullName;
        }

        return null;
    }
    public static string GetFolderPath(string folder)
    {
        if (folder.StartsWith(":"))
        {
            folder = folder[1..];
            return FindFolderBelow(folder);
        }

        return Path.IsPathRooted(folder) ? folder : Path.GetFullPath(folder);
    }

    public static string FindFolderBelow(string folder)
    {
        string current = AppDomain.CurrentDomain.BaseDirectory;

        while (current != null)
        {
            if (Directory.Exists(Path.Combine(current, folder)))
                return Path.Combine(current, folder);

            current = Directory.GetParent(current)?.FullName;
        }

        return null;
    }
    public static string GetUserDownloadPath() { try { return Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders\").GetValue("{374DE290-123F-4565-9164-39C4925E467B}").ToString(); } catch (Exception) { return null; } }
    public static string DownloadToString(string url, int timeoutMs = 30000)
    {
        try
        {
            using HttpClient client = new() { Timeout = TimeSpan.FromMilliseconds(timeoutMs) };
            return client.GetAsync(url).Result.Content.ReadAsStringAsync().Result;
        }
        catch (Exception e)
        {
            Log($"Download failed {e.Message} [Url: {url ?? "Null"}]");
        }

        return null;
    }

    public static MemoryStream DownloadFile(string url, int timeoutMs = 30000)
    {
        MemoryStream ms = new();

        try
        {
            using HttpClient client = new() { Timeout = TimeSpan.FromMilliseconds(timeoutMs) };
            client.GetAsync(url).Result.Content.CopyToAsync(ms).Wait();
        }
        catch (Exception e)
        {
            Log($"Download failed {e.Message} [Url: {url ?? "Null"}]");
        }

        return ms;
    }

    public static bool DownloadFile(string url, string filename, int timeoutMs = 30000, bool overwrite = true)
    {
        try
        {
            using HttpClient client = new() { Timeout = TimeSpan.FromMilliseconds(timeoutMs) };
            using FileStream fs = new(filename, overwrite ? FileMode.Create : FileMode.CreateNew);
            client.GetAsync(url).Result.Content.CopyToAsync(fs).Wait();

            return true;
        }
        catch (Exception e)
        {
            Log($"Download failed {e.Message} [Url: {url ?? "Null"}, Path: {filename ?? "Null"}]");
        }

        return false;
    }
    public static string FixFileUrl(string url)
    {
        try
        {
            if (url == null || url.Length < 5)
                return url;

            if (url[..5].ToLower() == "file:")
                return new Uri(url).LocalPath;
        }
        catch { }

        return url;
    }

    /// <summary>
    /// Convert Windows lnk file path to target path
    /// </summary>
    /// <param name="filepath">lnk file path</param>
    /// <returns>targetPath or null</returns>
    public static string GetLnkTargetPath(string filepath)
    {
        try
        {
            // Using dynamic COM
            // ref: https://stackoverflow.com/a/49198242/9070784
            dynamic windowsShell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell", true)!);
            dynamic shortcut = windowsShell!.CreateShortcut(filepath);
            string targetPath = shortcut.TargetPath;

            if (string.IsNullOrEmpty(targetPath))
            {
                throw new InvalidOperationException("TargetPath is empty.");
            }

            return targetPath;
        }
        catch (Exception e)
        {
            Log($"Resolving Windows Link failed {e.Message} [FilePath: {filepath}]");

            return null;
        }
    }

    public static string GetBytesReadable(nuint i)
    {
        // Determine the suffix and readable value
        string suffix;
        double readable;
        if (i >= 0x1000000000000000) // Exabyte
        {
            suffix = "EB";
            readable = i >> 50;
        }
        else if (i >= 0x4000000000000) // Petabyte
        {
            suffix = "PB";
            readable = i >> 40;
        }
        else if (i >= 0x10000000000) // Terabyte
        {
            suffix = "TB";
            readable = i >> 30;
        }
        else if (i >= 0x40000000) // Gigabyte
        {
            suffix = "GB";
            readable = i >> 20;
        }
        else if (i >= 0x100000) // Megabyte
        {
            suffix = "MB";
            readable = i >> 10;
        }
        else if (i >= 0x400) // Kilobyte
        {
            suffix = "KB";
            readable = i;
        }
        else
        {
            return i.ToString("0 B"); // Byte
        }
        // Divide by 1024 to get fractional value
        readable /= 1024;
        // Return formatted number with suffix
        return readable.ToString("0.## ") + suffix;
    }
    static List<PerformanceCounter> gpuCounters;
    public static void GetGPUCounters()
    {
        PerformanceCounterCategory category = new("GPU Engine");
        string[] counterNames = category.GetInstanceNames();
        gpuCounters = new List<PerformanceCounter>();

        foreach (string counterName in counterNames)
            if (counterName.EndsWith("engtype_3D"))
                foreach (var counter in category.GetCounters(counterName))
                    if (counter.CounterName == "Utilization Percentage")
                        gpuCounters.Add(counter);
    }
    public static float GetGPUUsage()
    {
        float result = 0f;

        try
        {
            if (gpuCounters == null) GetGPUCounters();

            gpuCounters.ForEach(x => { _ = x.NextValue(); });
            Thread.Sleep(1000);
            gpuCounters.ForEach(x => { result += x.NextValue(); });

        }
        catch (Exception e) { Log($"[GPUUsage] Error {e.Message}"); result = -1f; GetGPUCounters(); }

        return result;
    }
    public static string GZipDecompress(string filename)
    {
        string newFileName = "";

        FileInfo fileToDecompress = new(filename);
        using (var originalFileStream = fileToDecompress.OpenRead())
        {
            string currentFileName = fileToDecompress.FullName;
            newFileName = currentFileName.Remove(currentFileName.Length - fileToDecompress.Extension.Length);

            using var decompressedFileStream = File.Create(newFileName);
            using GZipStream decompressionStream = new(originalFileStream, CompressionMode.Decompress);
            decompressionStream.CopyTo(decompressedFileStream);
        }

        return newFileName;
    }

    public static Dictionary<string, string> ParseQueryString(ReadOnlySpan<char> query)
    {
        Dictionary<string, string> dict = [];

        int nameStart   = 0;
        int equalPos    = -1;
        for (int i = 0; i < query.Length; i++)
        {
            if (query[i] == '=')
                equalPos = i;
            else if (query[i] == '&')
            {
                if (equalPos == -1)
                    dict[query[nameStart..i].ToString()] = null;
                else
                    dict[query[nameStart..equalPos].ToString()] = query.Slice(equalPos + 1, i - equalPos - 1).ToString();

                equalPos    = -1;
                nameStart   = i + 1;
            }
        }

        if (nameStart < query.Length - 1)
        {
            if (equalPos == -1)
                dict[query[nameStart..].ToString()] = null;
            else
                dict[query[nameStart..equalPos].ToString()] = query.Slice(equalPos + 1, query.Length - equalPos - 1).ToString();
        }

        return dict;
    }

    public unsafe static string BytePtrToStringUTF8(byte* bytePtr)
        => Marshal.PtrToStringUTF8((nint)bytePtr);

    public static System.Windows.Media.Color WinFormsToWPFColor(System.Drawing.Color sColor)
        => System.Windows.Media.Color.FromArgb(sColor.A, sColor.R, sColor.G, sColor.B);
    public static System.Drawing.Color WPFToWinFormsColor(System.Windows.Media.Color wColor)
        => System.Drawing.Color.FromArgb(wColor.A, wColor.R, wColor.G, wColor.B);

    public static System.Windows.Media.Color VorticeToWPFColor(Vortice.Mathematics.Color sColor)
        => System.Windows.Media.Color.FromArgb(sColor.A, sColor.R, sColor.G, sColor.B);
    public static Vortice.Mathematics.Color WPFToVorticeColor(System.Windows.Media.Color wColor)
        => new Vortice.Mathematics.Color(wColor.R, wColor.G, wColor.B, wColor.A);

    public static double SWFREQ_TO_TICKS =  10000000.0 / Stopwatch.Frequency;
    public static string ToHexadecimal(byte[] bytes)
    {
        StringBuilder hexBuilder = new();
        for (int i = 0; i < bytes.Length; i++)
        {
            hexBuilder.Append(bytes[i].ToString("x2"));
        }
        return hexBuilder.ToString();
    }
    public static int GCD(int a, int b) => b == 0 ? a : GCD(b, a % b);
    public static string TicksToTime(long ticks) => new TimeSpan(ticks).ToString();
    public static void Log(string msg) { try { Debug.WriteLine($"[{DateTime.Now:hh.mm.ss.fff}] {msg}"); } catch (Exception) { Debug.WriteLine($"[............] [MediaFramework] {msg}"); } }

    [GeneratedRegex("[^a-z0-9]extended", RegexOptions.IgnoreCase)]
    private static partial Regex RxExtended();
    [GeneratedRegex("[^a-z0-9]directors.cut", RegexOptions.IgnoreCase)]
    private static partial Regex RxDirectorsCut();
    [GeneratedRegex(@"(^|[^a-z0-9])(s|season)[^a-z0-9]*(?<season>[0-9]{1,2})[^a-z0-9]*(e|episode|part)[^a-z0-9]*(?<episode>[0-9]{1,2})($|[^a-z0-9])", RegexOptions.IgnoreCase)]

    // s|season 01 ... e|episode|part 01
    private static partial Regex RxSeasonEpisode1();
    [GeneratedRegex(@"(^|[^a-z0-9])(?<season>[0-9]{1,2})x(?<episode>[0-9]{1,2})($|[^a-z0-9])", RegexOptions.IgnoreCase)]
    // 01x01
    private static partial Regex RxSeasonEpisode2();
    // TODO: in case of single season should check only for e|episode|part 01
    [GeneratedRegex(@"(^|[^a-z0-9])(episode|part)[^a-z0-9]*(?<episode>[0-9]{1,2})($|[^a-z0-9])", RegexOptions.IgnoreCase)]
    private static partial Regex RxEpisodePart();
    [GeneratedRegex("[^a-z0-9]brrip", RegexOptions.IgnoreCase)]
    private static partial Regex RxBrrip();

    [GeneratedRegex("[^a-z0-9][0-9]{3,4}p", RegexOptions.IgnoreCase)]
    private static partial Regex RxResolution();
    [GeneratedRegex(@"[^a-z0-9](?<year>(19|20)[0-9][0-9])($|[^a-z0-9])", RegexOptions.IgnoreCase)]
    private static partial Regex RxYear();
    [GeneratedRegex(@"\s{2,}")]
    private static partial Regex RxSpaces();
    [GeneratedRegex(@"[^a-z0-9]$", RegexOptions.IgnoreCase)]
    private static partial Regex RxNonAlphaNumeric();

    public static string TruncateString(string str, int maxLength, string suffix = "...")
    {
        if (string.IsNullOrEmpty(str))
            return str;

        if (str.Length <= maxLength)
            return str;

        int availableLength = maxLength - suffix.Length;

        if (availableLength <= 0)
        {
            return suffix.Substring(0, Math.Min(maxLength, suffix.Length));
        }

        return str.Substring(0, availableLength) + suffix;
    }

    // TODO: L: move to app, using event
    public static void PlayCompletionSound()
    {
        string soundPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/completion.mp3");

        if (!File.Exists(soundPath))
        {
            return;
        }

        UI(() =>
        {
            try
            {
                // play completion sound
                System.Windows.Media.MediaPlayer mp = new();
                mp.Open(new Uri(soundPath));
                mp.Play();
            }
            catch
            {
                // ignored
            }
        });
    }

    public static string CommandToText(this Command cmd)
    {
        if (cmd.TargetFilePath.Any(char.IsWhiteSpace))
        {
            return $"& \"{cmd.TargetFilePath}\" {cmd.Arguments}";
        }

        return cmd.ToString();
    }
}

using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Media.Ocr;
using Windows.Storage.Streams;
using Color = System.Drawing.Color;
using ImageFormat = System.Drawing.Imaging.ImageFormat;
using PixelFormat = System.Drawing.Imaging.PixelFormat;

namespace FlyleafLib.MediaPlayer;

#nullable enable

public unsafe class SubtitlesOCR
{
    private readonly Config.SubtitlesConfig _config;
    private int _subNum;

    private readonly CancellationTokenSource?[] _ctss;
    private readonly object[] _lockers;
    private IOCRService? _ocrService;

    public SubtitlesOCR(Config.SubtitlesConfig config, int subNum)
    {
        _config = config;
        _subNum = subNum;

        _lockers = new object[subNum];
        _ctss = new CancellationTokenSource[subNum];
        for (int i = 0; i < subNum; i++)
        {
            _lockers[i] = new object();
            _ctss[i] = new CancellationTokenSource();
        }
    }

    /// <summary>
    /// Try to initialize OCR Engine
    /// </summary>
    /// <param name="lang">OCR Language</param>
    /// <param name="err">expected initialize error</param>
    /// <returns>whether to success to initialize</returns>
    public bool TryInitialize(int subIndex, Language lang, out string err)
    {
        lang = GetLanguageWithFallback(subIndex, lang);

        // Retaining engines will increase memory usage, so they are created and discarded on the fly.
        IOCRService ocrService = _config[subIndex].OCREngine switch
        {
            SubOCREngineType.Tesseract => new TesseractOCRService(_config),
            SubOCREngineType.MicrosoftOCR => new MicrosoftOCRService(_config),
            _ => throw new InvalidOperationException(),
        };

        if (!ocrService.TryInitialize(lang, out err))
        {
            return false;
        }

        _ocrService = ocrService;

        err = "";
        return true;
    }

    /// <summary>
    /// Do OCR
    /// </summary>
    /// <param name="subIndex">0: Primary, 1: Secondary</param>
    /// <param name="subs">List of subtitle data for OCR</param>
    /// <param name="startTime">Timestamp to start OCR</param>
    public void Do(int subIndex, List<SubtitleData> subs, TimeSpan? startTime = null)
    {
        if (_ocrService == null)
            throw new InvalidOperationException("ocrService is not initialized. you must call TryInitialize() first");

        if (subs.Count == 0 || !subs[0].IsBitmap)
            return;

        // Cancel preceding OCR
        TryCancelWait(subIndex);

        lock (_lockers[subIndex])
        {
            // NOTE: important to dispose inside lock
            using IOCRService ocrService = _ocrService;

            _ctss[subIndex] = new CancellationTokenSource();

            int startIndex = 0;
            // Start OCR from the current playback point
            if (startTime.HasValue)
            {
                int match = subs.FindIndex(s => s.StartTime >= startTime);
                if (match != -1)
                {
                    // Do from 5 previous subtitles
                    startIndex = Math.Max(0, match - 5);
                }
            }

            for (int i = 0; i < subs.Count; i++)
            {
                if (_ctss[subIndex]!.Token.IsCancellationRequested)
                {
                    foreach (var sub in subs)
                    {
                        sub.Dispose();
                    }

                    break;
                }

                int index = (startIndex + i) % subs.Count;

                SubtitleBitmapData? bitmap = subs[index].Bitmap;
                if (bitmap == null)
                    continue;

                // TODO: L: If it's disposed, do I need to cancel it later?
                subs[index].Text = Process(ocrService, subIndex, bitmap);
                if (!string.IsNullOrEmpty(subs[index].Text))
                {
                    // If OCR succeeds, dispose of it (if it fails, leave it so that it can be displayed in the sidebar).
                    subs[index].Dispose();
                }
            }

            if (!_ctss[subIndex]!.Token.IsCancellationRequested)
            {
                // TODO: L: Notify, express completion in some way
                Utils.PlayCompletionSound();
            }
        }
    }

    private Language GetLanguageWithFallback(int subIndex, Language lang)
    {
        if (lang == Language.Unknown)
        {
            // fallback to user set language
            lang = subIndex == 0 ? _config.LanguageFallbackPrimary : _config.LanguageFallbackSecondary;
        }

        return lang;
    }

    public void TryCancelWait(int subIndex)
    {
        if (_ctss[subIndex] != null)
        {
            // Cancel if preceding OCR is running
            _ctss[subIndex]!.Cancel();

            // Wait until it is canceled by taking a lock
            lock (_lockers[subIndex])
            {
                _ctss[subIndex]?.Dispose();
                _ctss[subIndex] = null;
            }
        }
    }

    public static string Process(IOCRService ocrService, int subIndex, SubtitleBitmapData sub)
    {
        (byte[] data, AVSubtitleRect rect) = sub.SubToBitmap(true);

        int width = rect.w;
        int height = rect.h;

        fixed (byte* ptr = data)
        {
            // TODO: L: Make it possible to set true here in config (should the bitmap itself have an invert function automatically?)
            Binarize(width, height, ptr, 4, true);
        }

        using Bitmap bitmap = new(width, height, PixelFormat.Format32bppArgb);
        BitmapData bitmapData = bitmap.LockBits(
            new Rectangle(0, 0, width, height),
            ImageLockMode.WriteOnly, bitmap.PixelFormat);
        Marshal.Copy(data, 0, bitmapData.Scan0, data.Length);
        bitmap.UnlockBits(bitmapData);

        // Perform preprocessing to improve accuracy before OCR (common processing independent of OCR method)
        using Bitmap ocrBitmap = Preprocess(bitmap);

        string ocrText = ocrService.RecognizeTextAsync(ocrBitmap).GetAwaiter().GetResult();
        string processedText = ocrService.PostProcess(ocrText);

        return processedText;
    }

    private static Bitmap Preprocess(Bitmap bitmap)
    {
        using Bitmap blackText = ImageProcessor.BlackText(bitmap);
        Bitmap padded = ImageProcessor.AddPadding(blackText, 20);

        return padded;
    }

    /// <summary>
    /// Perform binarization on bitmaps
    /// </summary>
    /// <param name="width"></param>
    /// <param name="height"></param>
    /// <param name="buffer"></param>
    /// <param name="pixelByte"></param>
    /// <param name="srcTextWhite"></param>
    private static void Binarize(int width, int height, byte* buffer, int pixelByte, bool srcTextWhite)
    {
        // Black text on white background
        byte white = 255;
        byte black = 0;
        if (srcTextWhite)
        {
            // The text is white on a black background, so invert it to black text.
            white = 0;
            black = 255;
        }

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                byte* pixel = buffer + (y * width + x) * pixelByte;
                // Take out the first R bits since they are already in grayscale
                int grayscale = pixel[0];
                byte binaryValue = grayscale < 128 ? black : white;
                pixel[0] = pixel[1] = pixel[2] = binaryValue;
            }
        }
    }

    public void Reset(int subIndex)
    {
        TryCancelWait(subIndex);
    }
}

public interface IOCRService : IDisposable
{
    bool TryInitialize(Language lang, out string err);
    Task<string> RecognizeTextAsync(Bitmap bitmap);
    string PostProcess(string text);
}

public class TesseractOCRService : IOCRService
{
    private readonly Config.SubtitlesConfig _config;
    private TesseractOCR.Engine? _ocrEngine;
    private bool _disposed;
    private Language? _lang;

    public TesseractOCRService(Config.SubtitlesConfig config)
    {
        _config = config;
    }

    public bool TryInitialize(Language lang, out string err)
    {
        _lang = lang;

        string iso6391 = lang.ISO6391;

        if (iso6391 == "nb")
        {
            // Norwegian Bokmål to Norwegian
            iso6391 = "no";
        }

        Dictionary<string, List<TesseractModel>> tesseractModels = TesseractModelLoader.GetAvailableModels();

        if (!tesseractModels.TryGetValue(iso6391, out List<TesseractModel>? models))
        {
            err = $"Language:{lang.TopEnglishName} ({iso6391}) is not available in Tesseract OCR, Please download a model in settings if available language.";

            return false;
        }

        TesseractModel model = models.First();
        if (_config.TesseractOcrRegions != null && models.Count >= 2)
        {
            // choose zh-CN or zh-TW (for Chinese)
            if (_config.TesseractOcrRegions.TryGetValue(iso6391, out string? langCode))
            {
                TesseractModel? m = models.FirstOrDefault(m => m.LangCode == langCode);
                if (m != null)
                {
                    model = m;
                }
            }
        }

        _ocrEngine = new TesseractOCR.Engine(
            TesseractModel.ModelsDirectory,
            model.Lang);

        bool isCJK = model.Lang is
            TesseractOCR.Enums.Language.Japanese or
            TesseractOCR.Enums.Language.Korean or
            TesseractOCR.Enums.Language.ChineseSimplified or
            TesseractOCR.Enums.Language.ChineseTraditional;

        if (isCJK)
        {
            // remove whitespace around word if CJK
            _ocrEngine.SetVariable("preserve_interword_spaces", 1);
        }

        err = string.Empty;
        return true;
    }

    public Task<string> RecognizeTextAsync(Bitmap bitmap)
    {
        if (_ocrEngine == null)
            throw new InvalidOperationException("ocrEngine is not initialized");

        using MemoryStream stream = new();

        // 32bit -> 24bit conversion
        Bitmap converted = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb);
        using (Graphics g = Graphics.FromImage(converted))
        {
            g.DrawImage(bitmap, 0, 0);
        }

        converted.Save(stream, ImageFormat.Bmp);

        stream.Position = 0;

        using var img = TesseractOCR.Pix.Image.LoadFromMemory(stream);

        using var page = _ocrEngine.Process(img);
        return Task.FromResult(page.Text);
    }

    public string PostProcess(string text)
    {
        var processedText = text;

        if (_lang == Language.English)
        {
            processedText = processedText
                .Replace("|", "I")
                .Replace("I1t's", "It's")
                .Replace("’'", "'");
        }

        // common
        processedText = processedText
            .Trim();

        return processedText;
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        _ocrEngine?.Dispose();
        _disposed = true;
    }
}

public class MicrosoftOCRService : IOCRService
{
    private readonly Config.SubtitlesConfig _config;
    private OcrEngine? _ocrEngine;
    private bool _isCJK;
    private bool _disposed;

    public MicrosoftOCRService(Config.SubtitlesConfig config)
    {
        _config = config;
    }

    public bool TryInitialize(Language lang, out string err)
    {
        string iso6391 = lang.ISO6391;

        string? langTag = null;

        if (_config.MsOcrRegions.TryGetValue(iso6391, out string? tag))
        {
            // If there is a preferred language region in the settings, it is given priority.
            langTag = tag;
        }
        else
        {
            var availableLangs = OcrEngine.AvailableRecognizerLanguages.ToList();

            // full match
            var match = availableLangs.FirstOrDefault(l => l.LanguageTag == iso6391);

            if (match == null)
            {
                // left match
                match = availableLangs.FirstOrDefault(l => l.LanguageTag.StartsWith($"{iso6391}-"));
            }

            if (match != null)
            {
                langTag = match.LanguageTag;
            }
        }

        if (langTag == null)
        {
            err = $"Language:{lang.TopEnglishName} ({iso6391}) is not available in Microsoft OCR, Please install an OCR engine if available language.";
            return false;
        }

        var language = new Windows.Globalization.Language(langTag);

        OcrEngine ocrEngine = OcrEngine.TryCreateFromLanguage(language);
        if (ocrEngine == null)
        {
            err = $"Language:{lang.TopEnglishName} ({iso6391}) is not available in Microsoft OCR (TryCreateFromLanguage), Please install an OCR engine if available language.";
            return false;
        }

        _ocrEngine = ocrEngine;
        _isCJK = langTag.StartsWith("zh", StringComparison.OrdinalIgnoreCase) || // Chinese
                 langTag.StartsWith("ja", StringComparison.OrdinalIgnoreCase);   // Japanese

        err = string.Empty;
        return true;
    }

    public async Task<string> RecognizeTextAsync(Bitmap bitmap)
    {
        if (_ocrEngine == null)
            throw new InvalidOperationException("ocrEngine is not initialized");

        using MemoryStream ms = new();

        bitmap.Save(ms, ImageFormat.Bmp);
        ms.Position = 0;

        using IRandomAccessStream randomAccessStream = ms.AsRandomAccessStream();
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(randomAccessStream);
        using SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync();

        OcrResult ocrResult = await _ocrEngine.RecognizeAsync(softwareBitmap);

        if (_isCJK)
        {
            // remove whitespace around word if CJK
            return string.Join(Environment.NewLine,
                ocrResult.Lines.Select(line => string.Concat(line.Words.Select(word => word.Text))));
        }

        return string.Join(Environment.NewLine, ocrResult.Lines.Select(l => l.Text));
    }

    public string PostProcess(string text)
    {
        return text;
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        // do nothing
        _disposed = true;
    }
}

public static class ImageProcessor
{
    /// <summary>
    /// Converts to black text on a white background
    /// </summary>
    /// <param name="original">original bitmap</param>
    /// <returns>processed bitmap</returns>
    public static Bitmap BlackText(Bitmap original)
    {
        Bitmap converted = new(original.Width, original.Height, original.PixelFormat);

        using (Graphics g = Graphics.FromImage(converted))
        {
            // Convert to black text on a white background
            g.Clear(Color.White);

            // Drawing images with alpha blending enabled
            g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;

            g.DrawImage(original, 0, 0);
        }

        return converted;
    }

    /// <summary>
    /// Add white padding around the bitmap
    /// </summary>
    /// <param name="original">original bitmap</param>
    /// <param name="padding">Size of padding to be added (in pixels)</param>
    /// <returns>padded bitmap</returns>
    public static Bitmap AddPadding(Bitmap original, int padding)
    {
        int newWidth = original.Width + padding * 2;
        int newHeight = original.Height + padding * 2;

        Bitmap paddedBitmap = new(newWidth, newHeight, original.PixelFormat);

        using (Graphics graphics = Graphics.FromImage(paddedBitmap))
        {
            // White background
            graphics.Clear(Color.White);

            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

            // Draw the original image in the center
            graphics.DrawImage(original, padding, padding, original.Width, original.Height);
        }

        return paddedBitmap;
    }

    /// <summary>
    /// Enlarge the size of the bitmap while maintaining the aspect ratio.
    /// </summary>
    /// <param name="original">source bitmap</param>
    /// <returns>processed bitmap</returns>
    private static Bitmap ResizeBitmap(Bitmap original, double scaleFactor)
    {
        // Calculate new size
        int newWidth = (int)(original.Width * scaleFactor);
        int newHeight = (int)(original.Height * scaleFactor);

        Bitmap resizedBitmap = new(newWidth, newHeight);

        using (Graphics graphics = Graphics.FromImage(resizedBitmap))
        {
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

            // resize
            graphics.DrawImage(original, 0, 0, newWidth, newHeight);
        }

        return resizedBitmap;
    }
}

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Interop;
using System.Windows;

namespace FlyleafLib;

public static partial class Utils
{
    public static class ZOrderHandler
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);

        enum GetWindow_Cmd : uint {
            GW_HWNDFIRST = 0,
            GW_HWNDLAST = 1,
            GW_HWNDNEXT = 2,
            GW_HWNDPREV = 3,
            GW_OWNER = 4,
            GW_CHILD = 5,
            GW_ENABLEDPOPUP = 6
        }

        public class ZOrder
        {
            public string window;
            public int order;
        }

        public class Owner
        {
            static int uniqueNameId = 0;

            public List<ZOrder> CurZOrder = null;
            public List<ZOrder> SavedZOrder = null;

            public Window Window;
            public IntPtr WindowHwnd;

            Dictionary<string, IntPtr> WindowNamesHandles = new();
            Dictionary<string, Window> WindowNamesWindows = new();

            public Owner(Window window, IntPtr windowHwnd)
            {
                Window = window;
                WindowHwnd = windowHwnd;

                // TBR: Stand alone
                //if (Window.Owner != null)
                //    Window.Owner.StateChanged += Window_StateChanged;
                //else

                Window.StateChanged += Window_StateChanged;
                lastState = Window.WindowState;

                // TBR: Minimize with WindowsKey + D for example will not fire (none of those)
                //HwndSource source = HwndSource.FromHwnd(WindowHwnd);
                //source.AddHook(new HwndSourceHook(WndProc));
            }

            public const Int32 WM_SYSCOMMAND = 0x112;
            public const Int32 SC_MAXIMIZE = 0xF030;
            private const int SC_MINIMIZE = 0xF020;
            private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
            {
                if (msg == WM_SYSCOMMAND)
                {
                    if (wParam.ToInt32() == SC_MINIMIZE)
                    {
                        Save();
                        //handled = true;
                    }
                }
                return IntPtr.Zero;
            }

            private IntPtr GetHandle(Window window)
            {
                IntPtr hwnd = IntPtr.Zero;

                if (string.IsNullOrEmpty(window.Name))
                {
                    hwnd = new WindowInteropHelper(window).Handle;
                    window.Name = "Zorder" + uniqueNameId++.ToString();
                    WindowNamesHandles.Add(window.Name, hwnd);
                    WindowNamesWindows.Add(window.Name, window);
                }
                else if (!WindowNamesHandles.ContainsKey(window.Name))
                {
                    hwnd = new WindowInteropHelper(window).Handle;
                    WindowNamesHandles.Add(window.Name, hwnd);
                    WindowNamesWindows.Add(window.Name, window);
                }
                else
                    hwnd = WindowNamesHandles[window.Name];

                return hwnd;
            }

            WindowState lastState = WindowState.Normal;

            private void Window_StateChanged(object sender, EventArgs e)
            {
                if (Window.OwnedWindows.Count < 2)
                    return;

                if (lastState == WindowState.Minimized)
                    Restore();
                else if (Window.WindowState == WindowState.Minimized)
                    Save();

                lastState = Window.WindowState;
            }

            public void Save()
            {
                SavedZOrder = GetZOrder();
                Debug.WriteLine("Saved");
                DumpZOrder(SavedZOrder);
            }

            public void Restore()
            {
                if (SavedZOrder == null)
                    return;

                Task.Run(() =>
                {
                    System.Threading.Thread.Sleep(50);

                    Application.Current.Dispatcher.Invoke(() =>
                    {
                        for (int i=0; i<SavedZOrder.Count; i++)
                            if (WindowNamesWindows.TryGetValue(SavedZOrder[i].window, out var window) && window.IsVisible)
                                window.Activate();

                        Debug.WriteLine("Restored");
                        DumpZOrder(GetZOrder());
                    });
                });
            }

            public List<ZOrder> GetZOrder()
            {
                List<ZOrder> zorders = new();

                foreach(Window window in Window.OwnedWindows)
                {
                    ZOrder zorder = new();
                    IntPtr curHwnd = GetHandle(window);
                    if (curHwnd == IntPtr.Zero)
                        continue;

                    zorder.window = window.Name;

                    while ((curHwnd = GetWindow(curHwnd, (uint)GetWindow_Cmd.GW_HWNDNEXT)) != WindowHwnd && curHwnd != IntPtr.Zero)
                        zorder.order++;

                    zorders.Add(zorder);
                }

                return zorders.OrderBy((o) => o.order).ToList<ZOrder>();
            }

            public void DumpZOrder(List<ZOrder> zorders)
            {
                for (int i=0; i<zorders.Count; i++)
                    Debug.WriteLine(zorders[i].order + " " + zorders[i].window + " | " + WindowNamesWindows[zorders[i].window].Background);
            }
        }

        public static Dictionary<IntPtr, Owner> Owners = new();

        public static void Register(Window window)
        {
            IntPtr hwnd = new WindowInteropHelper(window).Handle;
            if (Owners.ContainsKey(hwnd))
                return;

            Owners.Add(hwnd, new Owner(window, hwnd));
        }
    }
}

using System.Collections.Generic;
using System.Threading;

using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DXGI;
using Vortice.Mathematics;
using Vortice;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;

using static FlyleafLib.Logger;

using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;

namespace FlyleafLib.MediaFramework.MediaRenderer;

unsafe public partial class Renderer
{
    static string[] pixelOffsets = new[] { "r", "g", "b", "a" };
    enum PSDefines { HDR, HLG, YUV }
    enum PSCase : int
    {
        None,
        HWD3D11VP,
        HWD3D11VPZeroCopy,
        HW,
        HWZeroCopy,

        RGBPacked,
        RGBPacked2,
        RGBPlanar,

        YUVPacked,
        YUVSemiPlanar,
        YUVPlanar,
        SwsScale
    }

    bool    checkHDR;
    PSCase  curPSCase;
    string  curPSUniqueId;
    float   curRatio = 1.0f;
    string  prevPSUniqueId;
    internal bool forceNotExtractor; // TBR: workaround until we separate the Extractor?

    Texture2DDescription[]          textDesc= new Texture2DDescription[4];
    ShaderResourceViewDescription[] srvDesc = new ShaderResourceViewDescription[4];
    SubresourceData[]               subData = new SubresourceData[1];
    Box                             cropBox = new(0, 0, 0, 0, 0, 1);

    void InitPS()
    {
        for (int i=0; i<textDesc.Length; i++)
        {
            textDesc[i].Usage               = ResourceUsage.Default;
            textDesc[i].BindFlags           = BindFlags.ShaderResource;// | BindFlags.RenderTarget;
            textDesc[i].SampleDescription   = new SampleDescription(1, 0);
            textDesc[i].ArraySize           = 1;
            textDesc[i].MipLevels           = 1;
        }

        for (int i=0; i<textDesc.Length; i++)
        {
            srvDesc[i].Texture2D        = new() { MipLevels = 1, MostDetailedMip = 0 };
            srvDesc[i].Texture2DArray   = new Texture2DArrayShaderResourceView() { ArraySize = 1, MipLevels = 1 };
        }
    }

    internal bool ConfigPlanes() //bool isNewInput = true) // currently not used
    {
        bool error = false;

        try
        {
            Monitor.Enter(VideoDecoder.lockCodecCtx);
            Monitor.Enter(lockDevice);

            // Don't use SCDisposed as we need to allow config planes even before swapchain creation
            // TBR: Possible run ConfigPlanes after swapchain creation instead (currently we don't access any resources of the swapchain here and is safe)
            if (Disposed || VideoStream == null)
                return false;

            VideoDecoder.DisposeFrame(LastFrame);

            curRatio    = VideoStream.AspectRatio.Value;
            IsHDR       = VideoStream.ColorSpace == ColorSpace.BT2020 && ( // TBR: Should transfer IsHDR to VideoStream? | Unspecified (Space/Transfer)
                          VideoStream.ColorTransfer == AVColorTransferCharacteristic.Smpte2084 ||  // PQ
                          VideoStream.ColorTransfer == AVColorTransferCharacteristic.AribStdB67);// HLG
            VideoRect   = new RawRect(0, 0, (int)VideoStream.Width, (int)VideoStream.Height);
            rotationLinesize
                        = false;
            UpdateRotation(_RotationAngle, false);

            if (IsHDR)
            {
                hdrPlusData = null;
                displayData = new();
                lightData   = new();
                checkHDR    = true;
            }
            else
                checkHDR = false;

            var oldVP = videoProcessor;

            // Defaults to D3D11VP
            //VideoProcessor = !VideoDecoder.VideoAccelerated || D3D11VPFailed || Config.Video.VideoProcessor == VideoProcessors.Flyleaf || (Config.Video.VideoProcessor == VideoProcessors.Auto && isHDR && !Config.Video.Deinterlace) ? VideoProcessors.Flyleaf : VideoProcessors.D3D11;

            // Defaults to FlyleafVP
            VideoProcessor = !D3D11VPFailed && VideoDecoder.VideoAccelerated &&
                (Config.Video.Deinterlace || Config.Video.VideoProcessor == VideoProcessors.D3D11) ?
                VideoProcessors.D3D11 : VideoProcessors.Flyleaf;

            textDesc[0].BindFlags &= ~BindFlags.RenderTarget; // Only D3D11VP without ZeroCopy requires it
            curPSCase = PSCase.None;
            prevPSUniqueId = curPSUniqueId;
            curPSUniqueId = "";

            Log.Debug($"Preparing planes for {VideoStream.PixelFormatStr} with {videoProcessor}");
            if ((VideoStream.PixelFormatDesc->flags & PixFmtFlags.Be) == 0) // We currently force SwsScale for BE (RGBA64/BGRA64 BE noted that could work as is?*)
            {
                if (videoProcessor == VideoProcessors.D3D11)
                {
                    if (oldVP != videoProcessor)
                    {
                        VideoDecoder.DisposeFrames();
                        Config.Video.Filters[VideoFilters.Brightness].Value = Config.Video.Filters[VideoFilters.Brightness].DefaultValue;
                        Config.Video.Filters[VideoFilters.Contrast].Value = Config.Video.Filters[VideoFilters.Contrast].DefaultValue;
                    }

                    inputColorSpace = new()
                    {
                        Usage           = 0,
                        RGB_Range       = VideoStream.AVStream->codecpar->color_range == AVColorRange.Jpeg ? (uint) 0 : 1,
                        YCbCr_Matrix    = VideoStream.ColorSpace != ColorSpace.BT601 ? (uint) 1 : 0,
                        YCbCr_xvYCC     = 0,
                        Nominal_Range   = VideoStream.AVStream->codecpar->color_range == AVColorRange.Jpeg ? (uint) 2 : 1
                    };

                    vpov?.Dispose();
                    vd1.CreateVideoProcessorOutputView(backBuffer, vpe, vpovd, out vpov);
                    vc.VideoProcessorSetStreamColorSpace(vp, 0, inputColorSpace);
                    vc.VideoProcessorSetOutputColorSpace(vp, outputColorSpace);

                    if (child != null)
                    {
                        child.vpov?.Dispose();
                        vd1.CreateVideoProcessorOutputView(child.backBuffer, vpe, vpovd, out child.vpov);
                    }

                    if (VideoDecoder.ZeroCopy)
                        curPSCase = PSCase.HWD3D11VPZeroCopy;
                    else
                    {
                        curPSCase = PSCase.HWD3D11VP;

                        textDesc[0].BindFlags |= BindFlags.RenderTarget;

                        cropBox.Right       = (int)VideoStream.Width;
                        textDesc[0].Width   = VideoStream.Width;
                        cropBox.Bottom      = (int)VideoStream.Height;
                        textDesc[0].Height  = VideoStream.Height;
                        textDesc[0].Format  = VideoDecoder.textureFFmpeg.Description.Format;
                    }
                }
                else if (!Config.Video.SwsForce || VideoDecoder.VideoAccelerated) // FlyleafVP
                {
                    List<string> defines = new();

                    if (oldVP != videoProcessor)
                    {
                        VideoDecoder.DisposeFrames();
                        Config.Video.Filters[VideoFilters.Brightness].Value = Config.Video.Filters[VideoFilters.Brightness].Minimum + ((Config.Video.Filters[VideoFilters.Brightness].Maximum - Config.Video.Filters[VideoFilters.Brightness].Minimum) / 2);
                        Config.Video.Filters[VideoFilters.Contrast].Value   = Config.Video.Filters[VideoFilters.Contrast].Minimum + ((Config.Video.Filters[VideoFilters.Contrast].Maximum - Config.Video.Filters[VideoFilters.Contrast].Minimum) / 2);
                    }

                    if (IsHDR)
                    {
                        // TBR: It currently works better without it
                        //if (VideoStream.ColorTransfer == AVColorTransferCharacteristic.AVCOL_TRC_ARIB_STD_B67)
                        //{
                        //    defines.Add(PSDefines.HLG.ToString());
                        //    curPSUniqueId += "g";
                        //}

                        curPSUniqueId += "h";
                        defines.Add(PSDefines.HDR.ToString());
                        psBufferData.coefsIndex = 0;
                        UpdateHDRtoSDR(false);
                    }
                    else
                        psBufferData.coefsIndex = VideoStream.ColorSpace == ColorSpace.BT709 ? 1 : 2;

                    for (int i=0; i<srvDesc.Length; i++)
                        srvDesc[i].ViewDimension = ShaderResourceViewDimension.Texture2D;

                    // 1. HW Decoding
                    if (VideoDecoder.VideoAccelerated)
                    {
                        defines.Add(PSDefines.YUV.ToString());

                        if (VideoDecoder.VideoStream.PixelComp0Depth > 8)
                        {
                            srvDesc[0].Format = Format.R16_UNorm;
                            srvDesc[1].Format = Format.R16G16_UNorm;
                        }
                        else
                        {
                            srvDesc[0].Format = Format.R8_UNorm;
                            srvDesc[1].Format = Format.R8G8_UNorm;
                        }

                        if (VideoDecoder.ZeroCopy)
                        {
                            curPSCase = PSCase.HWZeroCopy;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            for (int i=0; i<srvDesc.Length; i++)
                                srvDesc[i].ViewDimension = ShaderResourceViewDimension.Texture2DArray;
                        }
                        else
                        {
                            curPSCase = PSCase.HW;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            cropBox.Right       = (int)VideoStream.Width;
                            textDesc[0].Width   = VideoStream.Width;
                            cropBox.Bottom      = (int)VideoStream.Height;
                            textDesc[0].Height  = VideoStream.Height;
                            textDesc[0].Format  = VideoDecoder.textureFFmpeg.Description.Format;
                        }

                        SetPS(curPSUniqueId, @"
    color = float4(
        Texture1.Sample(Sampler, input.Texture).r,
        Texture2.Sample(Sampler, input.Texture).rg,
        1.0);
    ", defines);
                    }

                    else if (VideoStream.IsRGB)
                    {
                        // [RGB0]32 | [RGBA]32 | [RGBA]64
                        if (VideoStream.PixelPlanes == 1 && (
                            VideoStream.PixelFormat == AVPixelFormat._0RGB  ||
                            VideoStream.PixelFormat == AVPixelFormat.Rgb0   ||
                            VideoStream.PixelFormat == AVPixelFormat._0BGR  ||
                            VideoStream.PixelFormat == AVPixelFormat.Bgr0   ||

                            VideoStream.PixelFormat == AVPixelFormat.Argb   ||
                            VideoStream.PixelFormat == AVPixelFormat.Rgba   ||
                            VideoStream.PixelFormat == AVPixelFormat.Abgr   ||
                            VideoStream.PixelFormat == AVPixelFormat.Bgra   ||

                            VideoStream.PixelFormat == AVPixelFormat.Rgba64le||
                            VideoStream.PixelFormat == AVPixelFormat.Bgra64le))
                        {
                            curPSCase = PSCase.RGBPacked;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            textDesc[0].Width   = VideoStream.Width;
                            textDesc[0].Height  = VideoStream.Height;

                            if (VideoStream.PixelComp0Depth > 8)
                            {
                                curPSUniqueId += "x";
                                textDesc[0].Format  = srvDesc[0].Format = Format.R16G16B16A16_UNorm;
                            }
                            else if (VideoStream.PixelComp0Depth > 4)
                                textDesc[0].Format  = srvDesc[0].Format = Format.R8G8B8A8_UNorm; // B8G8R8X8_UNorm for 0[rgb]?

                            string offsets = "";
                            for (int i = 0; i < VideoStream.PixelComps.Length; i++)
                                offsets += pixelOffsets[(int) (VideoStream.PixelComps[i].offset / Math.Ceiling(VideoStream.PixelComp0Depth / 8.0))];

                            curPSUniqueId += offsets;

                            if (VideoStream.PixelComps.Length > 3)
                                SetPS(curPSUniqueId, $"color = Texture1.Sample(Sampler, input.Texture).{offsets};");
                            else
                                SetPS(curPSUniqueId, $"color = float4(Texture1.Sample(Sampler, input.Texture).{offsets}, 1.0);");
                        }

                        // [BGR/RGB]16
                        else if (VideoStream.PixelPlanes == 1 && (
                            VideoStream.PixelFormat == AVPixelFormat.Rgb444le||
                            VideoStream.PixelFormat == AVPixelFormat.Bgr444le))
                        {
                            curPSCase = PSCase.RGBPacked2;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            textDesc[0].Width   = VideoStream.Width;
                            textDesc[0].Height  = VideoStream.Height;

                            textDesc[0].Format  = srvDesc[0].Format = Format.B4G4R4A4_UNorm;

                            if (VideoStream.PixelFormat == AVPixelFormat.Rgb444le)
                            {
                                curPSUniqueId += "a";
                                SetPS(curPSUniqueId, $"color = float4(Texture1.Sample(Sampler, input.Texture).rgb, 1.0);");
                            }
                            else
                            {
                                curPSUniqueId += "b";
                                SetPS(curPSUniqueId, $"color = float4(Texture1.Sample(Sampler, input.Texture).bgr, 1.0);");
                            }
                        }

                        // GBR(A) <=16
                        else if (VideoStream.PixelPlanes > 2 && VideoStream.PixelComp0Depth <= 16)
                        {
                            curPSCase = PSCase.RGBPlanar;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            for (int i=0; i<VideoStream.PixelPlanes; i++)
                            {
                                textDesc[i].Width   = VideoStream.Width;
                                textDesc[i].Height  = VideoStream.Height;
                            }

                            string shader = @"
        color.g = Texture1.Sample(Sampler, input.Texture).r;
        color.b = Texture2.Sample(Sampler, input.Texture).r;
        color.r = Texture3.Sample(Sampler, input.Texture).r;
    ";

                            if (VideoStream.PixelPlanes == 4)
                            {
                                curPSUniqueId += "x";

                                shader += @"
        color.a = Texture4.Sample(Sampler, input.Texture).r;
    ";
                            }

                            if (VideoStream.PixelComp0Depth > 8)
                            {
                                curPSUniqueId += VideoStream.PixelComp0Depth;

                                for (int i=0; i<VideoStream.PixelPlanes; i++)
                                    textDesc[i].Format = srvDesc[i].Format = Format.R16_UNorm;

                                shader += @"
        color = color * pow(2, " + (16 - VideoStream.PixelComp0Depth) + @");
    ";
                            }
                            else
                            {
                                curPSUniqueId += "b";

                                for (int i=0; i<VideoStream.PixelPlanes; i++)
                                    textDesc[i].Format = srvDesc[i].Format = Format.R8_UNorm;
                            }

                            // if (VideoStream.PixelPlanes != 4) // TBR: seems causing issues
                            SetPS(curPSUniqueId, shader + @"
        color.a = 1;
    ", defines);
                        }
                    }

                    else // YUV
                    {
                        defines.Add(PSDefines.YUV.ToString());

                        if (VideoStream.PixelPlanes == 1 && (
                            VideoStream.PixelFormat == AVPixelFormat.Y210le  || // Not tested
                            VideoStream.PixelFormat == AVPixelFormat.Yuyv422 ||
                            VideoStream.PixelFormat == AVPixelFormat.Yvyu422 ||
                            VideoStream.PixelFormat == AVPixelFormat.Uyvy422 ))
                        {
                            curPSCase = PSCase.YUVPacked;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            psBufferData.texWidth = 1.0f / (VideoStream.Width >> 1);
                            textDesc[0].Width   = VideoStream.Width;
                            textDesc[0].Height  = VideoStream.Height;

                            if (VideoStream.PixelComp0Depth > 8)
                            {
                                curPSUniqueId += $"{VideoStream.Width}_";
                                textDesc[0].Format  = Format.Y210;
                                srvDesc[0].Format   = Format.R16G16B16A16_UNorm;
                            }
                            else
                            {
                                curPSUniqueId += $"{VideoStream.Width}";
                                textDesc[0].Format  = Format.YUY2;
                                srvDesc[0].Format   = Format.R8G8B8A8_UNorm;
                            }

                            string header = @"
        float  posx = input.Texture.x - (texWidth * 0.25);
        float  fx = frac(posx / texWidth);
        float  pos1 = posx + ((0.5 - fx) * texWidth);
        float  pos2 = posx + ((1.5 - fx) * texWidth);

        float4 c1 = Texture1.Sample(Sampler, float2(pos1, input.Texture.y));
        float4 c2 = Texture1.Sample(Sampler, float2(pos2, input.Texture.y));

    ";
                            if (VideoStream.PixelFormat == AVPixelFormat.Yuyv422 ||
                                VideoStream.PixelFormat == AVPixelFormat.Y210le)
                            {
                                curPSUniqueId += $"a";

                                SetPS(curPSUniqueId, header + @"
        float  leftY    = lerp(c1.r, c1.b, fx * 2);
        float  rightY   = lerp(c1.b, c2.r, fx * 2 - 1);
        float2 outUV    = lerp(c1.ga, c2.ga, fx);
        float  outY     = lerp(leftY, rightY, step(0.5, fx));
        color = float4(outY, outUV, 1.0);
    ", defines);
                            } else if (VideoStream.PixelFormat == AVPixelFormat.Yvyu422)
                            {
                                curPSUniqueId += $"b";

                                SetPS(curPSUniqueId, header + @"
        float  leftY    = lerp(c1.r, c1.b, fx * 2);
        float  rightY   = lerp(c1.b, c2.r, fx * 2 - 1);
        float2 outUV    = lerp(c1.ag, c2.ag, fx);
        float  outY     = lerp(leftY, rightY, step(0.5, fx));
        color = float4(outY, outUV, 1.0);
    ", defines);
                            } else if (VideoStream.PixelFormat == AVPixelFormat.Uyvy422)
                            {
                                curPSUniqueId += $"c";

                                SetPS(curPSUniqueId, header + @"
        float  leftY    = lerp(c1.g, c1.a, fx * 2);
        float  rightY   = lerp(c1.a, c2.g, fx * 2 - 1);
        float2 outUV    = lerp(c1.rb, c2.rb, fx);
        float  outY     = lerp(leftY, rightY, step(0.5, fx));
        color = float4(outY, outUV, 1.0);
    ", defines);
                            }
                        }

                        // Y_UV | nv12,nv21,nv24,nv42,p010le,p016le,p410le,p416le | (log2_chroma_w != log2_chroma_h / Interleaved) (? nv16,nv20le,p210le,p216le)
                        // This covers all planes == 2 YUV (Semi-Planar)
                        else if (VideoStream.PixelPlanes == 2) // && VideoStream.PixelSameDepth) && !VideoStream.PixelInterleaved)
                        {
                            curPSCase = PSCase.YUVSemiPlanar;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            textDesc[0].Width   = VideoStream.Width;
                            textDesc[0].Height  = VideoStream.Height;
                            textDesc[1].Width   = VideoStream.PixelFormatDesc->log2_chroma_w > 0 ? (VideoStream.Width  + 1) >> VideoStream.PixelFormatDesc->log2_chroma_w : VideoStream.Width  >> VideoStream.PixelFormatDesc->log2_chroma_w;
                            textDesc[1].Height  = VideoStream.PixelFormatDesc->log2_chroma_h > 0 ? (VideoStream.Height + 1) >> VideoStream.PixelFormatDesc->log2_chroma_h : VideoStream.Height >> VideoStream.PixelFormatDesc->log2_chroma_h;

                            string offsets = VideoStream.PixelComps[1].offset > VideoStream.PixelComps[2].offset ? "gr" : "rg";

                            if (VideoStream.PixelComp0Depth > 8)
                            {
                                curPSUniqueId += "x";
                                textDesc[0].Format  = srvDesc[0].Format = Format.R16_UNorm;
                                textDesc[1].Format  = srvDesc[1].Format = Format.R16G16_UNorm;
                            }
                            else
                            {
                                textDesc[0].Format = srvDesc[0].Format = Format.R8_UNorm;
                                textDesc[1].Format = srvDesc[1].Format = Format.R8G8_UNorm;
                            }

                            SetPS(curPSUniqueId, @"
    color = float4(
        Texture1.Sample(Sampler, input.Texture).r,
        Texture2.Sample(Sampler, input.Texture)." + offsets + @",
        1.0);
    ", defines);
                        }

                        // Y_U_V
                        else if (VideoStream.PixelPlanes > 2)
                        {
                            curPSCase = PSCase.YUVPlanar;
                            curPSUniqueId += ((int)curPSCase).ToString();

                            textDesc[0].Width   = textDesc[3].Width = VideoStream.Width;
                            textDesc[0].Height  = textDesc[3].Height= VideoStream.Height;
                            textDesc[1].Width   = textDesc[2].Width = VideoStream.PixelFormatDesc->log2_chroma_w > 0 ? (VideoStream.Width  + 1) >> VideoStream.PixelFormatDesc->log2_chroma_w : VideoStream.Width  >> VideoStream.PixelFormatDesc->log2_chroma_w;
                            textDesc[1].Height  = textDesc[2].Height= VideoStream.PixelFormatDesc->log2_chroma_h > 0 ? (VideoStream.Height + 1) >> VideoStream.PixelFormatDesc->log2_chroma_h : VideoStream.Height >> VideoStream.PixelFormatDesc->log2_chroma_h;

                            string shader = @"
        color.r = Texture1.Sample(Sampler, input.Texture).r;
        color.g = Texture2.Sample(Sampler, input.Texture).r;
        color.b = Texture3.Sample(Sampler, input.Texture).r;
    ";

                            if (VideoStream.PixelPlanes == 4)
                            {
                                curPSUniqueId += "x";

                                shader += @"
        color.a = Texture4.Sample(Sampler, input.Texture).r;
    ";
                            }

                            if (VideoStream.PixelComp0Depth > 8)
                            {
                                curPSUniqueId += VideoStream.PixelComp0Depth;

                                for (int i=0; i<VideoStream.PixelPlanes; i++)
                                    textDesc[i].Format = srvDesc[i].Format = Format.R16_UNorm;

                                shader += @"
        color = color * pow(2, " + (16 - VideoStream.PixelComp0Depth) + @");
    ";
                            }
                            else
                            {
                                curPSUniqueId += "b";

                                for (int i=0; i<VideoStream.PixelPlanes; i++)
                                    textDesc[i].Format = srvDesc[i].Format = Format.R8_UNorm;
                            }

                            SetPS(curPSUniqueId, shader + @"
        color.a = 1;
    ", defines);
                        }
                    }
                }
            }

            if (textDesc[0].Format != Format.Unknown && !Device.CheckFormatSupport(textDesc[0].Format).HasFlag(FormatSupport.Texture2D))
            {
                Log.Warn($"GPU does not support {textDesc[0].Format} texture format");
                curPSCase = PSCase.None;
            }

            if (curPSCase == PSCase.None)
            {
                Log.Warn($"{VideoStream.PixelFormatStr} not supported. Falling back to SwsScale");

                if (!VideoDecoder.SetupSws())
                {
                    Log.Error($"SwsScale setup failed");
                    return false;
                }

                curPSCase = PSCase.SwsScale;
                curPSUniqueId = ((int)curPSCase).ToString();

                textDesc[0].Width   = VideoStream.Width;
                textDesc[0].Height  = VideoStream.Height;
                textDesc[0].Format  = srvDesc[0].Format = Format.R8G8B8A8_UNorm;
                srvDesc[0].ViewDimension = ShaderResourceViewDimension.Texture2D;

                // TODO: should add HDR?
                SetPS(curPSUniqueId, @"
color = float4(Texture1.Sample(Sampler, input.Texture).rgb, 1.0);
");
            }

            //AV_PIX_FMT_FLAG_ALPHA (currently used only for RGBA?)
            //context.OMSetBlendState(curPSCase == PSCase.RGBPacked || (curPSCase == PSCase.RGBPlanar && VideoStream.PixelPlanes == 4) ? blendStateAlpha : null);
            context.OMSetBlendState(curPSCase == PSCase.RGBPacked ? blendStateAlpha : null);

            Log.Debug($"Prepared planes for {VideoStream.PixelFormatStr} with {videoProcessor} [{curPSCase}]");

            return true;

        }
        catch (Exception e)
        {
            Log.Error($"{VideoStream.PixelFormatStr} not supported? ({e.Message}");
            error = true;
            return false;

        }
        finally
        {
            if (!error && curPSCase != PSCase.None)
            {
                context.UpdateSubresource(psBufferData, psBuffer);

                if (ControlHandle != IntPtr.Zero || SwapChainWinUIClbk != null)
                    SetViewport();
                else if (!forceNotExtractor)
                    PrepareForExtract();

                if (child != null)
                {
                    //replica.ConfigPlanes();
                    child.curRatio      = curRatio;
                    child.VideoRect     = VideoRect;
                    child.videoProcessor= videoProcessor;
                    child.SetViewport();
                }
            }
            Monitor.Exit(lockDevice);
            Monitor.Exit(VideoDecoder.lockCodecCtx);
        }
    }
    internal VideoFrame FillPlanes(AVFrame* frame)
    {
        try
        {
            VideoFrame mFrame = new();
            mFrame.timestamp = (long)(frame->pts * VideoStream.Timebase) - VideoDecoder.Demuxer.StartTime;
            if (CanTrace) Log.Trace($"Processes {Utils.TicksToTime(mFrame.timestamp)}");

            if (checkHDR)
            {
                // TODO Dolpy Vision / Vivid
                //var hdrSideDolpyDynamic = av_frame_get_side_data(frame, AVFrameSideDataType.AV_FRAME_DATA_DOVI_METADATA);

                var hdrSideDynamic = av_frame_get_side_data(frame, AVFrameSideDataType.DynamicHdrPlus);

                if (hdrSideDynamic != null && hdrSideDynamic->data != null)
                {
                    hdrPlusData = (AVDynamicHDRPlus*) hdrSideDynamic->data;
                    UpdateHDRtoSDR();
                }
                else
                {
                    var lightSide   = av_frame_get_side_data(frame, AVFrameSideDataType.ContentLightLevel);
                    var displaySide = av_frame_get_side_data(frame, AVFrameSideDataType.MasteringDisplayMetadata);

                    if (lightSide != null && lightSide->data != null && ((AVContentLightMetadata*)lightSide->data)->MaxCLL != 0)
                    {
                        lightData = *((AVContentLightMetadata*) lightSide->data);
                        checkHDR = false;
                        UpdateHDRtoSDR();
                    }

                    if (displaySide != null && displaySide->data != null && ((AVMasteringDisplayMetadata*)displaySide->data)->has_luminance != 0)
                    {
                        displayData = *((AVMasteringDisplayMetadata*) displaySide->data);
                        checkHDR = false;
                        UpdateHDRtoSDR();
                    }
                }
            }

            if (curPSCase == PSCase.HWZeroCopy)
            {
                mFrame.srvs         = new ID3D11ShaderResourceView[2];
                srvDesc[0].Texture2DArray.FirstArraySlice = srvDesc[1].Texture2DArray.FirstArraySlice = (uint) frame->data[1];

                mFrame.srvs[0]      = Device.CreateShaderResourceView(VideoDecoder.textureFFmpeg, srvDesc[0]);
                mFrame.srvs[1]      = Device.CreateShaderResourceView(VideoDecoder.textureFFmpeg, srvDesc[1]);

                mFrame.avFrame = av_frame_alloc();
                av_frame_move_ref(mFrame.avFrame, frame);
                return mFrame;
            }

            else if (curPSCase == PSCase.HW)
            {
                mFrame.textures     = new ID3D11Texture2D[1];
                mFrame.srvs         = new ID3D11ShaderResourceView[2];

                mFrame.textures[0]  = Device.CreateTexture2D(textDesc[0]);
                context.CopySubresourceRegion(
                    mFrame.textures[0], 0, 0, 0, 0, // dst
                    VideoDecoder.textureFFmpeg, (uint) frame->data[1],  // src
                    cropBox); // crop decoder's padding

                mFrame.srvs[0]      = Device.CreateShaderResourceView(mFrame.textures[0], srvDesc[0]);
                mFrame.srvs[1]      = Device.CreateShaderResourceView(mFrame.textures[0], srvDesc[1]);
            }

            else if (curPSCase == PSCase.HWD3D11VPZeroCopy)
            {
                mFrame.avFrame = av_frame_alloc();
                av_frame_move_ref(mFrame.avFrame, frame);
                return mFrame;
            }

            else if (curPSCase == PSCase.HWD3D11VP)
            {
                mFrame.textures     = new ID3D11Texture2D[1];
                mFrame.textures[0]  = Device.CreateTexture2D(textDesc[0]);
                context.CopySubresourceRegion(
                    mFrame.textures[0], 0, 0, 0, 0, // dst
                    VideoDecoder.textureFFmpeg, (uint) frame->data[1],  // src
                    cropBox); // crop decoder's padding
            }

            else if (curPSCase == PSCase.SwsScale)
            {
                mFrame.textures         = new ID3D11Texture2D[1];
                mFrame.srvs             = new ID3D11ShaderResourceView[1];

                sws_scale(VideoDecoder.swsCtx, frame->data.ToRawArray(), frame->linesize.ToArray(), 0, frame->height, VideoDecoder.swsData.ToRawArray(), VideoDecoder.swsLineSize.ToArray());

                subData[0].DataPointer  = VideoDecoder.swsData[0];
                subData[0].RowPitch     = (uint)VideoDecoder.swsLineSize[0];

                mFrame.textures[0]      = Device.CreateTexture2D(textDesc[0], subData);
                mFrame.srvs[0]          = Device.CreateShaderResourceView(mFrame.textures[0], srvDesc[0]);
            }

            else
            {
                mFrame.textures = new ID3D11Texture2D[VideoStream.PixelPlanes];
                mFrame.srvs     = new ID3D11ShaderResourceView[VideoStream.PixelPlanes];

                bool newRotationLinesize = false;
                for (int i = 0; i < VideoStream.PixelPlanes; i++)
                {
                    if (frame->linesize[i] < 0)
                    {
                        // Negative linesize for vertical flipping [TBR: might required for HW as well? (SwsScale does that)] http://ffmpeg.org/doxygen/trunk/structAVFrame.html#aa52bfc6605f6a3059a0c3226cc0f6567
                        newRotationLinesize     = true;
                        subData[0].RowPitch     = (uint)(-1 * frame->linesize[i]);
                        subData[0].DataPointer  = frame->data[i];
                        subData[0].DataPointer -= (nint)((subData[0].RowPitch * (VideoStream.Height - 1)));
                    }
                    else
                    {
                        newRotationLinesize     = false;
                        subData[0].RowPitch     = (uint)frame->linesize[i];
                        subData[0].DataPointer  = frame->data[i];
                    }

                    if (subData[0].RowPitch < textDesc[i].Width) // Prevent reading more than the actual data (Access Violation #424)
                    {
                        av_frame_unref(frame);
                        return null;
                    }

                    mFrame.textures[i]  = Device.CreateTexture2D(textDesc[i], subData);
                    mFrame.srvs[i]      = Device.CreateShaderResourceView(mFrame.textures[i], srvDesc[i]);
                }

                if (newRotationLinesize != rotationLinesize)
                {
                    rotationLinesize = newRotationLinesize;
                    UpdateRotation(_RotationAngle);
                }
            }

            av_frame_unref(frame);
            return mFrame;
        }
        catch (Exception e)
        {
            av_frame_unref(frame);
            Log.Error($"Failed to process frame ({e.Message})");
            return null;
        }
    }

    void SetPS(string uniqueId, string sampleHLSL, List<string> defines = null)
    {
        if (curPSUniqueId == prevPSUniqueId)
            return;

        ShaderPS?.Dispose();
        ShaderPS = ShaderCompiler.CompilePS(Device, uniqueId, sampleHLSL, defines);
        context.PSSetShader(ShaderPS);
    }
}

using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using FlyleafLib;
using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Views;
using WpfColorFontDialog;
using KeyBinding = FlyleafLib.MediaPlayer.KeyBinding;

namespace LLPlayer.Services;

public class AppActions
{
    private readonly Player _player;
    private readonly AppConfig _config;
    private FlyleafHost? _flyleafHost => _player.Host as FlyleafHost;
    private readonly IDialogService _dialogService;

#pragma warning disable CS9264
    public AppActions(Player player, AppConfig config, IDialogService dialogService)
    {
        _player = player;
        _config = config;
        _dialogService = dialogService;

        CustomActions = GetCustomActions();

        RegisterCustomKeyBindingsAction();
    }
#pragma warning restore CS9264

    private void RegisterCustomKeyBindingsAction()
    {
        // Since the action name is defined in PlayerConfig, get the Action name from there and register delegate.
        foreach (var binding in _player.Config.Player.KeyBindings.Keys)
        {
            if (binding.Action == KeyBindingAction.Custom && !string.IsNullOrEmpty(binding.ActionName))
            {
                if (Enum.TryParse(binding.ActionName, out CustomKeyBindingAction key))
                {
                    binding.SetAction(CustomActions[key], binding.IsKeyUp);
                }
            }
        }
    }

    public Dictionary<CustomKeyBindingAction, Action> CustomActions { get; }

    private Dictionary<CustomKeyBindingAction, Action> GetCustomActions()
    {
        return new Dictionary<CustomKeyBindingAction, Action>
        {
            [CustomKeyBindingAction.OpenNextFile] = CmdOpenNextFile.Execute,
            [CustomKeyBindingAction.OpenPrevFile] = CmdOpenPrevFile.Execute,
            [CustomKeyBindingAction.OpenCurrentPath] = CmdOpenCurrentPath.Execute,

            [CustomKeyBindingAction.SubsPositionUp] = CmdSubsPositionUp.Execute,
            [CustomKeyBindingAction.SubsPositionDown] = CmdSubsPositionDown.Execute,
            [CustomKeyBindingAction.SubsSizeIncrease] = CmdSubsSizeIncrease.Execute,
            [CustomKeyBindingAction.SubsSizeDecrease] = CmdSubsSizeDecrease.Execute,
            [CustomKeyBindingAction.SubsPrimarySizeIncrease] = CmdSubsPrimarySizeIncrease.Execute,
            [CustomKeyBindingAction.SubsPrimarySizeDecrease] = CmdSubsPrimarySizeDecrease.Execute,
            [CustomKeyBindingAction.SubsSecondarySizeIncrease] = CmdSubsSecondarySizeIncrease.Execute,
            [CustomKeyBindingAction.SubsSecondarySizeDecrease] = CmdSubsSecondarySizeDecrease.Execute,
            [CustomKeyBindingAction.SubsDistanceIncrease] = CmdSubsDistanceIncrease.Execute,
            [CustomKeyBindingAction.SubsDistanceDecrease] = CmdSubsDistanceDecrease.Execute,

            [CustomKeyBindingAction.SubsTextCopy] = () => CmdSubsTextCopy.Execute(false),
            [CustomKeyBindingAction.SubsPrimaryTextCopy] = () => CmdSubsPrimaryTextCopy.Execute(false),
            [CustomKeyBindingAction.SubsSecondaryTextCopy] = () => CmdSubsSecondaryTextCopy.Execute(false),
            [CustomKeyBindingAction.ToggleSubsAutoTextCopy] = CmdToggleSubsAutoTextCopy.Execute,
            [CustomKeyBindingAction.ToggleSidebarShowSecondary] = CmdToggleSidebarShowSecondary.Execute,
            [CustomKeyBindingAction.ToggleSidebarShowOriginalText] = CmdToggleSidebarShowOriginalText.Execute,
            [CustomKeyBindingAction.ActivateSubsSearch] = CmdActivateSubsSearch.Execute,

            [CustomKeyBindingAction.ToggleSidebar] = CmdToggleSidebar.Execute,
            [CustomKeyBindingAction.ToggleDebugOverlay] = CmdToggleDebugOverlay.Execute,
            [CustomKeyBindingAction.ToggleAlwaysOnTop] = CmdToggleAlwaysOnTop.Execute,

            [CustomKeyBindingAction.OpenWindowSettings] = CmdOpenWindowSettings.Execute,
            [CustomKeyBindingAction.OpenWindowSubsDownloader] = CmdOpenWindowSubsDownloader.Execute,
            [CustomKeyBindingAction.OpenWindowSubsExporter] = CmdOpenWindowSubsExporter.Execute,
            [CustomKeyBindingAction.OpenWindowCheatSheet] = CmdOpenWindowCheatSheet.Execute,

            [CustomKeyBindingAction.AppNew] = CmdAppNew.Execute,
            [CustomKeyBindingAction.AppClone] = CmdAppClone.Execute,
            [CustomKeyBindingAction.AppRestart] = CmdAppRestart.Execute,
            [CustomKeyBindingAction.AppExit] = CmdAppExit.Execute,
        };
    }

    public static List<KeyBinding> DefaultCustomActionsMap()
    {
        return
        [
            new() { ActionName = nameof(CustomKeyBindingAction.OpenNextFile), Key = Key.Right, Alt = true, Shift = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.OpenPrevFile), Key = Key.Left, Alt = true, Shift = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsPositionUp), Key = Key.Up, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsPositionDown), Key = Key.Down, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsSizeIncrease), Key = Key.Right, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsSizeDecrease), Key = Key.Left, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsSecondarySizeIncrease), Key = Key.Right, Ctrl = true, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsSecondarySizeDecrease), Key = Key.Left, Ctrl = true, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsDistanceIncrease), Key = Key.Up, Ctrl = true, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsDistanceDecrease), Key = Key.Down, Ctrl = true, Shift = true },
            new() { ActionName = nameof(CustomKeyBindingAction.SubsPrimaryTextCopy), Key = Key.C, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.ToggleSubsAutoTextCopy), Key = Key.A, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.ActivateSubsSearch), Key = Key.F, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.ToggleSidebar), Key = Key.B, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.ToggleDebugOverlay), Key = Key.D, Ctrl = true, Shift = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.ToggleAlwaysOnTop), Key = Key.T, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.OpenWindowSettings), Key = Key.OemComma, Ctrl = true, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.OpenWindowCheatSheet), Key = Key.F1, IsKeyUp = true },
            new() { ActionName = nameof(CustomKeyBindingAction.AppNew), Key = Key.N, Ctrl = true, IsKeyUp = true },
        ];
    }

    // ReSharper disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract

    #region Command used in key

    public DelegateCommand CmdOpenNextFile => field ??= new(() =>
    {
        OpenNextPrevInternal(isNext: true);
    });

    public DelegateCommand CmdOpenPrevFile => field ??= new(() =>
    {
        OpenNextPrevInternal(isNext: false);
    });

    private void OpenNextPrevInternal(bool isNext)
    {
        var playlist = _player.Playlist;
        if (playlist.Url == null)
            return;

        string url = playlist.Url;

        try
        {
            (string? prev, string? next) = FileHelper.GetNextAndPreviousFile(url);
            if (isNext && next != null)
            {
                _player.OpenAsync(next);
            }
            else if (!isNext && prev != null)
            {
                _player.OpenAsync(prev);
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show($"OpenNextPrevFile is failed: {ex.Message}");
        }
    }

    public DelegateCommand CmdOpenCurrentPath => field ??= new(() =>
    {
        var playlist = _player.Playlist;
        if (playlist.Selected == null)
            return;

        string url = playlist.Url;

        bool isFile = File.Exists(url);
        bool isUrl = url.StartsWith("http:", StringComparison.OrdinalIgnoreCase) ||
                     url.StartsWith("https:", StringComparison.OrdinalIgnoreCase);

        if (!isFile && !isUrl)
        {
            return;
        }

        if (isUrl)
        {
            // fix url
            url = playlist.Selected.DirectUrl;
        }

        // Open folder or URL
        string? fileName = isFile ? Path.GetDirectoryName(url) : url;

        Process.Start(new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = true,
            Verb = "open"
        });
    });

    public DelegateCommand CmdSubsPositionUp => field ??= new(() =>
    {
        SubsPositionUpActionInternal(true);
    });

    public DelegateCommand CmdSubsPositionDown => field ??= new(() =>
    {
        SubsPositionUpActionInternal(false);
    });

    public DelegateCommand CmdSubsSizeIncrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.All, increase: true);
    });

    public DelegateCommand CmdSubsSizeDecrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.All, increase: false);
    });

    public DelegateCommand CmdSubsPrimarySizeIncrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.Primary, increase: true);
    });

    public DelegateCommand CmdSubsPrimarySizeDecrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.Primary, increase: false);
    });

    public DelegateCommand CmdSubsSecondarySizeIncrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.Secondary, increase: true);
    });

    public DelegateCommand CmdSubsSecondarySizeDecrease => field ??= new(() =>
    {
        SubsSizeActionInternal(SubsSizeActionType.Secondary, increase: false);
    });

    private void SubsSizeActionInternal(SubsSizeActionType type, bool increase)
    {
        var primary = _player.Subtitles[0];
        var secondary = _player.Subtitles[1];

        if (type is SubsSizeActionType.All or SubsSizeActionType.Primary)
        {
            // TODO: L: Match scaling ratios in text and bitmap subtitles
            if (primary.Enabled && (!primary.IsBitmap || !string.IsNullOrEmpty(primary.Data.Text)))
            {
                _config.Subs.SubsFontSize += _config.Subs.SubsFontSizeOffset * (increase ? 1 : -1);
            }
            else if (primary.Enabled && primary.IsBitmap)
            {
                _player.Subtitles[0].Data.BitmapPosition.ConfScale += _config.Subs.SubsBitmapScaleOffset / 100.0 * (increase ? 1.0 : -1.0);
            }
        }

        if (type is SubsSizeActionType.All or SubsSizeActionType.Secondary)
        {
            if (secondary.Enabled && (!secondary.IsBitmap || !string.IsNullOrEmpty(secondary.Data.Text)))
            {
                _config.Subs.SubsFontSize2 += _config.Subs.SubsFontSizeOffset * (increase ? 1 : -1);
            }
            else if (secondary.Enabled && secondary.IsBitmap)
            {
                _player.Subtitles[1].Data.BitmapPosition.ConfScale += _config.Subs.SubsBitmapScaleOffset / 100.0 * (increase ? 1.0 : -1.0);
            }
        }
    }

    public DelegateCommand CmdSubsDistanceIncrease => field ??= new(() =>
    {
        SubsDistanceActionInternal(true);
    });

    public DelegateCommand CmdSubsDistanceDecrease => field ??= new(() =>
    {
        SubsDistanceActionInternal(false);
    });

    private void SubsDistanceActionInternal(bool increase)
    {
        _config.Subs.SubsDistance += _config.Subs.SubsDistanceOffset * (increase ? 1 : -1);
    }

    public DelegateCommand<bool?> CmdSubsTextCopy => field ??= new((suppressOsd) =>
    {
        if (!_player.Subtitles[0].Enabled && !_player.Subtitles[1].Enabled)
        {
            return;
        }

        string text = _player.Subtitles[0].Data.Text;
        if (!string.IsNullOrEmpty(_player.Subtitles[1].Data.Text))
        {
            text += Environment.NewLine + "( " + _player.Subtitles[1].Data.Text + " )";
        }

        if (!string.IsNullOrEmpty(text))
        {
            try
            {
                WindowsClipboard.SetText(text);
                if (!suppressOsd.HasValue || !suppressOsd.Value)
                {
                    _player.OSDMessage = "Copy All Subtitle Text";
                }
            }
            catch
            {
                // ignored
            }
        }
    });

    public DelegateCommand<bool?> CmdSubsPrimaryTextCopy => field ??= new((suppressOsd) =>
    {
        SubsTextCopyInternal(0, suppressOsd);
    });

    public DelegateCommand<bool?> CmdSubsSecondaryTextCopy => field ??= new((suppressOsd) =>
    {
        SubsTextCopyInternal(1, suppressOsd);
    });

    private void SubsTextCopyInternal(int subIndex, bool? suppressOsd)
    {
        if (!_player.Subtitles[subIndex].Enabled)
        {
            return;
        }

        string text = _player.Subtitles[subIndex].Data.Text;

        if (!string.IsNullOrEmpty(text))
        {
            try
            {
                WindowsClipboard.SetText(text);
                if (!suppressOsd.HasValue || !suppressOsd.Value)
                {
                    _player.OSDMessage = $"Copy {(subIndex == 0 ? "Primary" : "Secondary")} Subtitle Text";
                }
            }
            catch
            {
                // ignored
            }
        }
    }

    public DelegateCommand CmdToggleSubsAutoTextCopy => field ??= new(() =>
    {
        _config.Subs.SubsAutoTextCopy = !_config.Subs.SubsAutoTextCopy;
    });

    public DelegateCommand CmdActivateSubsSearch => field ??= new(() =>
    {
        if (_flyleafHost is { IsFullScreen: true })
        {
            return;
        }

        _config.ShowSidebar = true;

        // for getting focus to TextBox
        _config.SidebarSearchActive = false;
        _config.SidebarSearchActive = true;
    });

    public DelegateCommand CmdToggleSidebarShowSecondary => field ??= new(() =>
    {
        _config.SidebarShowSecondary = !_config.SidebarShowSecondary;
    });

    public DelegateCommand CmdToggleSidebarShowOriginalText => field ??= new(() =>
    {
        _config.SidebarShowOriginalText = !_config.SidebarShowOriginalText;
    });

    public DelegateCommand CmdToggleSidebar => field ??= new(() =>
    {
        _config.ShowSidebar = !_config.ShowSidebar;
    });

    public DelegateCommand CmdToggleDebugOverlay => field ??= new(() =>
    {
        _config.ShowDebug = !_config.ShowDebug;
    });

    public DelegateCommand CmdToggleAlwaysOnTop => field ??= new(() =>
    {
        _config.AlwaysOnTop = !_config.AlwaysOnTop;
    });

    public DelegateCommand CmdOpenWindowSettings => field ??= new(() =>
    {
        if (_player.IsPlaying)
        {
            _player.Pause();
        }

        // Detects configuration changes necessary for restart
        // TODO: L: refactor
        bool requiredRestart = false;

        _config.PropertyChanged += ConfigOnPropertyChanged;
        _player.Config.Subtitles.WhisperCppConfig.PropertyChanged += ConfigOnPropertyChanged;

        void ConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case nameof(_config.IsDarkTitlebar):
                case nameof(_player.Config.Subtitles.WhisperCppConfig.RuntimeLibraries):
                    requiredRestart = true;
                    break;
            }
        }

        _player.Activity.ForceFullActive();
        _dialogService.ShowSingleton(nameof(SettingsDialog), result =>
        {
            // Activate as it may be minimized for some reason
            if (!Application.Current.MainWindow!.IsActive)
            {
                Application.Current.MainWindow!.Activate();
            }

            _config.PropertyChanged -= ConfigOnPropertyChanged;
            _player.Config.Subtitles.WhisperCppConfig.PropertyChanged += ConfigOnPropertyChanged;

            if (result.Result == ButtonResult.OK)
            {
                SaveAllConfig();

                if (requiredRestart)
                {
                    // Show confirmation dialog
                    MessageBoxResult confirm = MessageBox.Show(
                        "A restart is required for the settings to take effect. Do you want to restart?",
                        "Confirm to restart",
                        MessageBoxButton.YesNo,
                        MessageBoxImage.Question
                    );

                    if (confirm == MessageBoxResult.Yes)
                    {
                        CmdAppRestart.Execute();
                    }
                }
            }
        }, false);
    });

    public DelegateCommand CmdOpenWindowSubsDownloader => field ??= new(() =>
    {
        _player.Activity.ForceFullActive();
        _dialogService.ShowSingleton(nameof(SubtitlesDownloaderDialog), true);
    });

    public DelegateCommand CmdOpenWindowSubsExporter => field ??= new(() =>
    {
        _player.Activity.ForceFullActive();
        _dialogService.ShowSingleton(nameof(SubtitlesExportDialog), _ =>
        {
            // Activate as it may be minimized for some reason
            if (!Application.Current.MainWindow!.IsActive)
            {
                Application.Current.MainWindow!.Activate();
            }
        }, false);
    });

    public DelegateCommand CmdOpenWindowCheatSheet => field ??= new(() =>
    {
        _player.Activity.ForceFullActive();
        _dialogService.ShowSingleton(nameof(CheatSheetDialog), true);
    });

    public DelegateCommand CmdAppNew => field ??= new(() =>
    {
        string exePath = Process.GetCurrentProcess().MainModule!.FileName;
        Process.Start(exePath);
    });

    public DelegateCommand CmdAppClone => field ??= new(() =>
    {
        string exePath = Process.GetCurrentProcess().MainModule!.FileName;

        ProcessStartInfo startInfo = new()
        {
            FileName = exePath,
            UseShellExecute = false
        };

        // Launch New App with current url if opened
        var prevPlaylist = _player.Playlist.Selected;
        if (prevPlaylist != null)
        {
            startInfo.ArgumentList.Add(prevPlaylist.DirectUrl);
        }

        Process.Start(startInfo);
    });

    public DelegateCommand CmdAppRestart => field ??= new(() =>
    {
        // Clone
        CmdAppClone.Execute();

        // Exit
        CmdAppExit.Execute();
    });

    public DelegateCommand CmdAppExit => field ??= new(() =>
    {
        Application.Current.Shutdown();
    });
    #endregion

    #region Command not used in key
    private static FontWeightConverter _fontWeightConv = new();
    private static FontStyleConverter _fontStyleConv = new();
    private static FontStretchConverter _fontStretchConv = new();

    public DelegateCommand CmdSetSubtitlesFont => field ??= new(() =>
    {
        ColorFontDialog dialog = new();

        dialog.Topmost = _config.AlwaysOnTop;
        dialog.Font = new FontInfo(new FontFamily(_config.Subs.SubsFontFamily), _config.Subs.SubsFontSize, (FontStyle)_fontStyleConv.ConvertFromString(_config.Subs.SubsFontStyle)!, (FontStretch)_fontStretchConv.ConvertFromString(_config.Subs.SubsFontStretch)!, (FontWeight)_fontWeightConv.ConvertFromString(_config.Subs.SubsFontWeight)!, new SolidColorBrush(Colors.Black));

        _player.Activity.ForceFullActive();

        double prevFontSize = dialog.Font.Size;

        if (dialog.ShowDialog() == true && dialog.Font != null)
        {
            _config.Subs.SubsFontFamily = dialog.Font.Family.ToString();
            _config.Subs.SubsFontWeight = dialog.Font.Weight.ToString();
            _config.Subs.SubsFontStretch = dialog.Font.Stretch.ToString();
            _config.Subs.SubsFontStyle = dialog.Font.Style.ToString();

            if (prevFontSize != dialog.Font.Size)
            {
                _config.Subs.SubsFontSize = dialog.Font.Size;
                _config.Subs.SubsFontSize2 = dialog.Font.Size; // change secondary as well
            }

            // update display of secondary subtitles
            _config.Subs.UpdateSecondaryFonts();
        }
    });

    public DelegateCommand CmdSetSubtitlesFont2 => field ??= new(() =>
    {
        ColorFontDialog dialog = new();

        dialog.Topmost = _config.AlwaysOnTop;
        dialog.Font = new FontInfo(new FontFamily(_config.Subs.SubsFontFamily2), _config.Subs.SubsFontSize2, (FontStyle)_fontStyleConv.ConvertFromString(_config.Subs.SubsFontStyle2)!, (FontStretch)_fontStretchConv.ConvertFromString(_config.Subs.SubsFontStretch2)!, (FontWeight)_fontWeightConv.ConvertFromString(_config.Subs.SubsFontWeight2)!, new SolidColorBrush(Colors.Black));

        _player.Activity.ForceFullActive();

        if (dialog.ShowDialog() == true && dialog.Font != null)
        {
            _config.Subs.SubsFontFamily2 = dialog.Font.Family.ToString();
            _config.Subs.SubsFontWeight2 = dialog.Font.Weight.ToString();
            _config.Subs.SubsFontStretch2 = dialog.Font.Stretch.ToString();
            _config.Subs.SubsFontStyle2 = dialog.Font.Style.ToString();

            _config.Subs.SubsFontSize2 = dialog.Font.Size;

            // update display of secondary subtitles
            _config.Subs.UpdateSecondaryFonts();
        }
    });

    public DelegateCommand CmdSetSubtitlesFontSidebar => field ??= new(() =>
    {
        ColorFontDialog dialog = new();

        dialog.Topmost = _config.AlwaysOnTop;
        dialog.Font = new FontInfo(new FontFamily(_config.SidebarFontFamily), _config.SidebarFontSize, FontStyles.Normal, FontStretches.Normal, (FontWeight)_fontWeightConv.ConvertFromString(_config.SidebarFontWeight)!, new SolidColorBrush(Colors.Black));

        _player.Activity.ForceFullActive();

        if (dialog.ShowDialog() == true && dialog.Font != null)
        {
            _config.SidebarFontFamily = dialog.Font.Family.ToString();
            _config.SidebarFontSize = dialog.Font.Size;
            _config.SidebarFontWeight = dialog.Font.Weight.ToString();
        }
    });

    public DelegateCommand CmdResetSubsPosition => field ??= new(() =>
    {
        // TODO: L: Reset bitmap as well
        _config.Subs.ResetSubsPosition();
    });

    public DelegateCommand<AspectRatio?> CmdChangeAspectRatio => field ??= new((ratio) =>
    {
        if (!ratio.HasValue)
            return;

        _player.Config.Video.AspectRatio = ratio.Value;
    });

    public DelegateCommand<SubPositionAlignment?> CmdSetSubPositionAlignment => field ??= new((alignment) =>
    {
        if (!alignment.HasValue)
            return;

        _config.Subs.SubsPositionAlignment = alignment.Value;
    });

    public DelegateCommand<SubPositionAlignment?> CmdSetSubPositionAlignmentWhenDual => field ??= new((alignment) =>
    {
        if (!alignment.HasValue)
            return;

        _config.Subs.SubsPositionAlignmentWhenDual = alignment.Value;
    });

    #endregion

    // ReSharper restore NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract

    // TODO: L: make it command?
    public void SaveAllConfig()
    {
        _config.Save(App.AppConfigPath);

        var opts = AppConfig.GetJsonSerializerOptions();
        _player.Config.Version = App.Version;
        _player.Config.Save(App.PlayerConfigPath, opts);

        Engine.Config.Version = App.Version;
        Engine.Config.Save(App.EngineConfigPath, opts);
    }

    private enum SubsSizeActionType
    {
        All,
        Primary,
        Secondary
    }

    private void SubsPositionUpActionInternal(bool up)
    {
        var primary = _player.Subtitles[0];
        var secondary = _player.Subtitles[1];

        bool bothEnabled = primary.Enabled && secondary.Enabled;

        if (bothEnabled || // both enabled
            primary.Enabled && !primary.IsBitmap || //  When text subtitles are enabled
            secondary.Enabled && !secondary.IsBitmap || !string.IsNullOrEmpty(primary.Data.Text) || //  If OCR subtitles are available
            !string.IsNullOrEmpty(secondary.Data.Text))
        {
            _config.Subs.SubsPosition += _config.Subs.SubsPositionOffset * (up ? -1 : 1);
        }
        else if (primary.IsBitmap || secondary.IsBitmap)
        {
            // For bitmap subtitles (absolute position)
            for (int i = 0; i < _player.Config.Subtitles.Max; i++)
            {
                _player.Subtitles[i].Data.BitmapPosition.ConfPos += _config.Subs.SubsPositionOffset * (up ? -1 : 1);
            }
        }
    }
}

// List of key actions to be added from the app side
public enum CustomKeyBindingAction
{
    [Description("Open Next File")]
    OpenNextFile,
    [Description("Open Previous File")]
    OpenPrevFile,
    [Description("Open Folder or URL of the currently opened file")]
    OpenCurrentPath,

    [Description("Subtitles Position Up")]
    SubsPositionUp,
    [Description("Subtitles Position Down")]
    SubsPositionDown,
    [Description("Subtitles Size Increase")]
    SubsSizeIncrease,
    [Description("Subtitles Size Decrease")]
    SubsSizeDecrease,
    [Description("Primary Subtitles Size Increase")]
    SubsPrimarySizeIncrease,
    [Description("Primary Subtitles Size Decrease")]
    SubsPrimarySizeDecrease,
    [Description("Secondary Subtitles Size Increase")]
    SubsSecondarySizeIncrease,
    [Description("Secondary Subtitles Size Decrease")]
    SubsSecondarySizeDecrease,
    [Description("Primary/Secondary Subtitles Distance Increase")]
    SubsDistanceIncrease,
    [Description("Primary/Secondary Subtitles Distance Decrease")]
    SubsDistanceDecrease,

    [Description("Copy All Subtiltes Text")]
    SubsTextCopy,
    [Description("Copy Primary Subtiltes Text")]
    SubsPrimaryTextCopy,
    [Description("Copy Secondary Subtiltes Text")]
    SubsSecondaryTextCopy,
    [Description("Toggle Auto Subtitles Text Copy")]
    ToggleSubsAutoTextCopy,

    [Description("Toggle Primary / Secondary in Subtitles Sidebar")]
    ToggleSidebarShowSecondary,
    [Description("Toggle to show original text in Subtitles Sidebar")]
    ToggleSidebarShowOriginalText,
    [Description("Activate Subtitles Search in Sidebar")]
    ActivateSubsSearch,

    [Description("Toggle Subitltes Sidebar")]
    ToggleSidebar,
    [Description("Toggle Debug Overlay")]
    ToggleDebugOverlay,
    [Description("Toggle Always On Top")]
    ToggleAlwaysOnTop,

    [Description("Open Settings Window")]
    OpenWindowSettings,
    [Description("Open Subtitles Downloader Window")]
    OpenWindowSubsDownloader,
    [Description("Open Subtitles Exporter Window")]
    OpenWindowSubsExporter,
    [Description("Open Cheat Sheet Window")]
    OpenWindowCheatSheet,

    [Description("Launch New Application")]
    AppNew,
    [Description("Launch Clone Application")]
    AppClone,
    [Description("Restart Application")]
    AppRestart,
    [Description("Exit Application")]
    AppExit,
}

public enum KeyBindingActionGroup
{
    Playback,
    Player,
    Audio,
    Video,
    Subtitles,
    SubtitlesPosition,
    Window,
    Other
}

public static class KeyBindingActionExtensions
{
    public static string ToString(this KeyBindingActionGroup group)
    {
        var str = group.ToString();
        if (group == KeyBindingActionGroup.SubtitlesPosition)
        {
            str = "Subtitles Position";
        }

        return str;
    }

    public static KeyBindingActionGroup ToGroup(this CustomKeyBindingAction action)
    {
        switch (action)
        {
            case CustomKeyBindingAction.OpenNextFile:
            case CustomKeyBindingAction.OpenPrevFile:
            case CustomKeyBindingAction.OpenCurrentPath:
                return KeyBindingActionGroup.Player; // TODO: L: ?

            case CustomKeyBindingAction.SubsPositionUp:
            case CustomKeyBindingAction.SubsPositionDown:
            case CustomKeyBindingAction.SubsSizeIncrease:
            case CustomKeyBindingAction.SubsSizeDecrease:
            case CustomKeyBindingAction.SubsPrimarySizeIncrease:
            case CustomKeyBindingAction.SubsPrimarySizeDecrease:
            case CustomKeyBindingAction.SubsSecondarySizeIncrease:
            case CustomKeyBindingAction.SubsSecondarySizeDecrease:
            case CustomKeyBindingAction.SubsDistanceIncrease:
            case CustomKeyBindingAction.SubsDistanceDecrease:
                return KeyBindingActionGroup.SubtitlesPosition;

            case CustomKeyBindingAction.SubsTextCopy:
            case CustomKeyBindingAction.SubsPrimaryTextCopy:
            case CustomKeyBindingAction.SubsSecondaryTextCopy:
            case CustomKeyBindingAction.ToggleSubsAutoTextCopy:
            case CustomKeyBindingAction.ToggleSidebarShowSecondary:
            case CustomKeyBindingAction.ToggleSidebarShowOriginalText:
            case CustomKeyBindingAction.ActivateSubsSearch:
                return KeyBindingActionGroup.Subtitles;

            case CustomKeyBindingAction.ToggleSidebar:
            case CustomKeyBindingAction.ToggleDebugOverlay:
            case CustomKeyBindingAction.ToggleAlwaysOnTop:
            case CustomKeyBindingAction.OpenWindowSettings:
            case CustomKeyBindingAction.OpenWindowSubsDownloader:
            case CustomKeyBindingAction.OpenWindowSubsExporter:
            case CustomKeyBindingAction.OpenWindowCheatSheet:
                return KeyBindingActionGroup.Window;

            // TODO: L: review group
            case CustomKeyBindingAction.AppNew:
            case CustomKeyBindingAction.AppClone:
            case CustomKeyBindingAction.AppRestart:
            case CustomKeyBindingAction.AppExit:
                return KeyBindingActionGroup.Other;

            default:
                return KeyBindingActionGroup.Other;
        }
    }

    public static KeyBindingActionGroup ToGroup(this KeyBindingAction action)
    {
        switch (action)
        {
            // TODO: L: review order and grouping
            case KeyBindingAction.ForceIdle:
            case KeyBindingAction.ForceActive:
            case KeyBindingAction.ForceFullActive:
                return KeyBindingActionGroup.Player;

            case KeyBindingAction.AudioDelayAdd:
            case KeyBindingAction.AudioDelayAdd2:
            case KeyBindingAction.AudioDelayRemove:
            case KeyBindingAction.AudioDelayRemove2:
            case KeyBindingAction.ToggleAudio:
            case KeyBindingAction.ToggleMute:
            case KeyBindingAction.VolumeUp:
            case KeyBindingAction.VolumeDown:
                return KeyBindingActionGroup.Audio;

            case KeyBindingAction.SubsDelayAddPrimary:
            case KeyBindingAction.SubsDelayAdd2Primary:
            case KeyBindingAction.SubsDelayRemovePrimary:
            case KeyBindingAction.SubsDelayRemove2Primary:
            case KeyBindingAction.SubsDelayAddSecondary:
            case KeyBindingAction.SubsDelayAdd2Secondary:
            case KeyBindingAction.SubsDelayRemoveSecondary:
            case KeyBindingAction.SubsDelayRemove2Secondary:
                return KeyBindingActionGroup.SubtitlesPosition;
            case KeyBindingAction.ToggleSubtitlesVisibility:
            case KeyBindingAction.ToggleSubtitlesVisibilityPrimary:
            case KeyBindingAction.ToggleSubtitlesVisibilitySecondary:
                return KeyBindingActionGroup.Subtitles;

            case KeyBindingAction.CopyToClipboard:
            case KeyBindingAction.CopyItemToClipboard:
                return KeyBindingActionGroup.Other; // TODO: L: ?

            case KeyBindingAction.OpenFromClipboard:
            case KeyBindingAction.OpenFromClipboardSafe:
            case KeyBindingAction.OpenFromFileDialog:
                return KeyBindingActionGroup.Player; // TODO: L: ?

            case KeyBindingAction.Stop:
            case KeyBindingAction.Pause:
            case KeyBindingAction.Play:
            case KeyBindingAction.TogglePlayPause:
            case KeyBindingAction.ToggleReversePlayback:
            case KeyBindingAction.ToggleLoopPlayback:
            case KeyBindingAction.SeekForward:
            case KeyBindingAction.SeekBackward:
            case KeyBindingAction.SeekForward2:
            case KeyBindingAction.SeekBackward2:
            case KeyBindingAction.SeekForward3:
            case KeyBindingAction.SeekBackward3:
            case KeyBindingAction.SeekForward4:
            case KeyBindingAction.SeekBackward4:
                return KeyBindingActionGroup.Playback;

            case KeyBindingAction.Flush:
            case KeyBindingAction.NormalScreen:
            case KeyBindingAction.FullScreen:
            case KeyBindingAction.ToggleFullScreen:
                return KeyBindingActionGroup.Player;

            case KeyBindingAction.ToggleVideo:
            case KeyBindingAction.ToggleKeepRatio:
            case KeyBindingAction.ToggleVideoAcceleration:
            case KeyBindingAction.TakeSnapshot:
            case KeyBindingAction.ToggleRecording:
                return KeyBindingActionGroup.Video;

            case KeyBindingAction.SubsPrevSeek:
            case KeyBindingAction.SubsCurSeek:
            case KeyBindingAction.SubsNextSeek:
            case KeyBindingAction.SubsPrevSeekFallback:
            case KeyBindingAction.SubsNextSeekFallback:
            case KeyBindingAction.SubsPrevSeek2:
            case KeyBindingAction.SubsCurSeek2:
            case KeyBindingAction.SubsNextSeek2:
            case KeyBindingAction.SubsPrevSeekFallback2:
            case KeyBindingAction.SubsNextSeekFallback2:
                return KeyBindingActionGroup.Subtitles;

            case KeyBindingAction.ShowNextFrame:
            case KeyBindingAction.ShowPrevFrame:
            case KeyBindingAction.SpeedAdd:
            case KeyBindingAction.SpeedAdd2:
            case KeyBindingAction.SpeedRemove:
            case KeyBindingAction.SpeedRemove2:
            case KeyBindingAction.ToggleSeekAccurate:
                return KeyBindingActionGroup.Playback;

            case KeyBindingAction.ResetAll:
            case KeyBindingAction.ResetSpeed:
            case KeyBindingAction.ResetRotation:
            case KeyBindingAction.ResetZoom:
            case KeyBindingAction.ZoomIn:
            case KeyBindingAction.ZoomOut:
                return KeyBindingActionGroup.Player;

            default:
                return KeyBindingActionGroup.Other;
        }
    }

    /// <summary>
    /// Gets the value of the Description attribute assigned to the Enum member.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static string GetDescription(this Enum value)
    {
        ArgumentNullException.ThrowIfNull(value);

        Type type = value.GetType();

        string name = value.ToString();

        MemberInfo[] member = type.GetMember(name);

        if (member.Length > 0)
        {
            if (Attribute.GetCustomAttribute(member[0], typeof(DescriptionAttribute)) is DescriptionAttribute attr)
            {
                return attr.Description;
            }
        }

        return name;
    }
}

global using System;
global using Flyleaf.FFmpeg;
global using static Flyleaf.FFmpeg.Raw;

using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace FlyleafLib;

public enum PixelFormatType
{
    Hardware,
    Software_Handled,
    Software_Sws
}
public enum MediaType
{
    Audio,
    Video,
    Subs,
    Data
}
public enum InputType
{
    File    = 0,
    UNC     = 1,
    Torrent = 2,
    Web     = 3,
    Unknown = 4
}
public enum HDRtoSDRMethod : int
{
    None        = 0,
    Aces        = 1,
    Hable       = 2,
    Reinhard    = 3
}
public enum VideoProcessors
{
    Auto,
    D3D11,
    Flyleaf,
}
public enum ZeroCopy : int
{
    Auto        = 0,
    Enabled     = 1,
    Disabled    = 2
}
public enum ColorSpace : int
{
    None        = 0,
    BT601       = 1,
    BT709       = 2,
    BT2020      = 3
}
public enum ColorRange : int
{
    None        = 0,
    Full        = 1,
    Limited     = 2
}

public enum SubOCREngineType
{
    Tesseract,
    MicrosoftOCR
}

public enum SubASREngineType
{
    [Description("whisper.cpp")]
    WhisperCpp,
    [Description("faster-whisper (Recommended)")]
    FasterWhisper
}

public class GPUOutput
{
    public static int GPUOutputIdGenerator;

    public int      Id          { get; set; }
    public string   DeviceName  { get; internal set; }
    public int      Left        { get; internal set; }
    public int      Top         { get; internal set; }
    public int      Right       { get; internal set; }
    public int      Bottom      { get; internal set; }
    public int      Width       => Right- Left;
    public int      Height      => Bottom- Top;
    public bool     IsAttached  { get; internal set; }
    public int      Rotation    { get; internal set; }

    public override string ToString()
    {
        int gcd = Utils.GCD(Width, Height);
        return $"{DeviceName,-20} [Id: {Id,-4}\t, Top: {Top,-4}, Left: {Left,-4}, Width: {Width,-4}, Height: {Height,-4}, Ratio: [" + (gcd > 0 ? $"{Width/gcd}:{Height/gcd}]" : "]");
    }
}

public class GPUAdapter
{
    public int      MaxHeight       { get; internal set; }
    public nuint    SystemMemory    { get; internal set; }
    public nuint    VideoMemory     { get; internal set; }
    public nuint    SharedMemory    { get; internal set; }

    public uint     Id              { get; internal set; }
    public string   Vendor          { get; internal set; }
    public string   Description     { get; internal set; }
    public long     Luid            { get; internal set; }
    public bool     HasOutput       { get; internal set; }
    public List<GPUOutput>
                    Outputs         { get; internal set; }

    public override string ToString()
        => (Vendor + " " + Description).PadRight(40) + $"[ID: {Id,-6}, LUID: {Luid,-6}, DVM: {Utils.GetBytesReadable(VideoMemory),-8}, DSM: {Utils.GetBytesReadable(SystemMemory),-8}, SSM: {Utils.GetBytesReadable(SharedMemory)}]";
}
public enum VideoFilters
{
    // Ensure we have the same values with Vortice.Direct3D11.VideoProcessorFilterCaps (d3d11.h) | we can extended if needed with other values

    Brightness          = 0x01,
    Contrast            = 0x02,
    Hue                 = 0x04,
    Saturation          = 0x08,
    NoiseReduction      = 0x10,
    EdgeEnhancement     = 0x20,
    AnamorphicScaling   = 0x40,
    StereoAdjustment    = 0x80
}

public struct AspectRatio : IEquatable<AspectRatio>
{
    public static readonly AspectRatio Keep     = new(-1, 1);
    public static readonly AspectRatio Fill     = new(-2, 1);
    public static readonly AspectRatio Custom   = new(-3, 1);
    public static readonly AspectRatio Invalid  = new(-999, 1);

    public static readonly List<AspectRatio> AspectRatios = new()
    {
        Keep,
        Fill,
        Custom,
        new AspectRatio(1, 1),
        new AspectRatio(4, 3),
        new AspectRatio(16, 9),
        new AspectRatio(16, 10),
        new AspectRatio(2.35f, 1),
    };

    public static implicit operator AspectRatio(string value) => new AspectRatio(value);

    public float Num { get; set; }
    public float Den { get; set; }

    public float Value
    {
        get => Num / Den;
        set  { Num = value; Den = 1; }
    }

    public string ValueStr
    {
        get => ToString();
        set => FromString(value);
    }

    public AspectRatio(float value) : this(value, 1) { }
    public AspectRatio(float num, float den) { Num = num; Den = den; }
    public AspectRatio(string value) { Num = Invalid.Num; Den = Invalid.Den; FromString(value); }

    public bool Equals(AspectRatio other) => Num == other.Num && Den == other.Den;
    public override bool Equals(object obj) => obj is AspectRatio o && Equals(o);
    public override int GetHashCode() => HashCode.Combine(Num, Den);
    public static bool operator ==(AspectRatio a, AspectRatio b) => a.Equals(b);
    public static bool operator !=(AspectRatio a, AspectRatio b) => !(a == b);

    public void FromString(string value)
    {
        if (value == "Keep")
            { Num = Keep.Num; Den = Keep.Den; return; }
        else if (value == "Fill")
            { Num = Fill.Num; Den = Fill.Den; return; }
        else if (value == "Custom")
            { Num = Custom.Num; Den = Custom.Den; return; }
        else if (value == "Invalid")
            { Num = Invalid.Num; Den = Invalid.Den; return; }

        string newvalue = value.ToString().Replace(',', '.');

        if (Regex.IsMatch(newvalue.ToString(), @"^\s*[0-9\.]+\s*[:/]\s*[0-9\.]+\s*$"))
        {
            string[] values = newvalue.ToString().Split(':');
            if (values.Length < 2)
                        values = newvalue.ToString().Split('/');

            Num = float.Parse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture);
            Den = float.Parse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture);
        }

        else if (float.TryParse(newvalue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out float result))
            { Num = result; Den = 1; }

        else
            { Num = Invalid.Num; Den = Invalid.Den; }
    }
    public override string ToString() => this == Keep ? "Keep" : (this == Fill ? "Fill" : (this == Custom ? "Custom" : (this == Invalid ? "Invalid" : $"{Num}:{Den}")));
}

class PlayerStats
{
    public long TotalBytes      { get; set; }
    public long VideoBytes      { get; set; }
    public long AudioBytes      { get; set; }
    public long FramesDisplayed { get; set; }
}
public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    //public bool DisableNotifications { get; set; }

    //private static bool IsUI() => System.Threading.Thread.CurrentThread.ManagedThreadId == System.Windows.Application.Current.Dispatcher.Thread.ManagedThreadId;

    protected bool Set<T>(ref T field, T value, bool check = true, [CallerMemberName] string propertyName = "")
    {
        //Utils.Log($"[===| {propertyName} |===] | Set | {IsUI()}");

        if (!check || !EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;

            //if (!DisableNotifications)
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

            return true;
        }

        return false;
    }

    protected bool SetUI<T>(ref T field, T value, bool check = true, [CallerMemberName] string propertyName = "")
    {
        //Utils.Log($"[===| {propertyName} |===] | SetUI | {IsUI()}");

        if (!check || !EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;

            //if (!DisableNotifications)
            Utils.UI(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));

            return true;
        }

        return false;
    }
    protected void Raise([CallerMemberName] string propertyName = "")
    {
        //Utils.Log($"[===| {propertyName} |===] | Raise | {IsUI()}");

        //if (!DisableNotifications)
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void RaiseUI([CallerMemberName] string propertyName = "")
    {
        //Utils.Log($"[===| {propertyName} |===] | RaiseUI | {IsUI()}");

        //if (!DisableNotifications)
        Utils.UI(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
    }
}

using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using LLPlayer.Views;

namespace LLPlayer;

public partial class App : PrismApplication
{
    public static string Name => "LLPlayer";
    public static string? CmdUrl { get; private set; } = null;
    public static string PlayerConfigPath { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LLPlayer.PlayerConfig.json");
    public static string EngineConfigPath { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LLPlayer.Engine.json");
    public static string AppConfigPath { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LLPlayer.Config.json");
    public static string CrashLogPath { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log");

    private readonly LogHandler Log;

    public App()
    {
        Log = new LogHandler("[App] [MainApp       ] ");
    }

    static App()
    {
        // Set thread culture to English and error messages to English
        Utils.SaveOriginalCulture();

        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
        CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry
            .Register<Player>(FlyleafLoader.CreateFlyleafPlayer)
            .RegisterSingleton<FlyleafManager>()
            .RegisterSingleton<IDialogService, ExtendedDialogService>();

        containerRegistry.RegisterDialogWindow<MyDialogWindow>();

        containerRegistry.RegisterDialog<SettingsDialog>();
        containerRegistry.RegisterDialog<SelectLanguageDialog>();
        containerRegistry.RegisterDialog<SubtitlesDownloaderDialog>();
        containerRegistry.RegisterDialog<SubtitlesExportDialog>();
        containerRegistry.RegisterDialog<CheatSheetDialog>();
        containerRegistry.RegisterDialog<WhisperModelDownloadDialog>();
        containerRegistry.RegisterDialog<WhisperEngineDownloadDialog>();
        containerRegistry.RegisterDialog<TesseractDownloadDialog>();
        containerRegistry.RegisterDialog<ErrorDialog>();
    }

    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        if (e.Args.Length == 1)
        {
            CmdUrl = e.Args[0];
        }

        // TODO: L: customizable?
        // Ensures that we have enough worker threads to avoid the UI from freezing or not updating on time
        ThreadPool.GetMinThreads(out int workers, out int ports);
        ThreadPool.SetMinThreads(workers + 6, ports + 6);

        // Start flyleaf engine
        FlyleafLoader.StartEngine();

        base.OnStartup(e);
    }

    private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        // Ignore WPF Clipboard exception
        if (e.Exception is COMException { ErrorCode: -2147221040 })
        {
            e.Handled = true;
        }

        if (!e.Handled)
        {
            Log.Error($"Unknown error occurred in App: {e.Exception}");
            Logger.ForceFlush();

            ErrorDialogHelper.ShowUnknownErrorPopup($"Unhandled Exception: {e.Exception.Message}", "Global", e.Exception);
            e.Handled = true;
        }
    }

    #region App Version
    public static string OSArchitecture => RuntimeInformation.OSArchitecture.ToString().ToLowerFirstChar();
    public static string ProcessArchitecture => RuntimeInformation.ProcessArchitecture.ToString().ToLowerFirstChar();

    private static string? _version;
    public static string Version
    {
        get
        {
            if (_version == null)
            {
                (_version, _commitHash) = GetVersion();
            }

            return _version;
        }
    }

    private static string? _commitHash;
    public static string CommitHash
    {
        get
        {
            if (_commitHash == null)
            {
                (_version, _commitHash) = GetVersion();
            }

            return _commitHash;
        }
    }

    private static (string version, string commitHash) GetVersion()
    {
        string exeLocation = Process.GetCurrentProcess().MainModule!.FileName;
        FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(exeLocation);

        Guards.ThrowIfNull(fvi.ProductVersion);

        var version = fvi.ProductVersion.Split("+");
        if (version.Length != 2)
        {
            throw new InvalidOperationException($"ProductVersion is invalid: {fvi.ProductVersion}");
        }

        return (version[0], version[1]);
    }
    #endregion
}

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;

using Vortice.DXGI;
using Vortice.Direct3D11;

using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;

using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;
using FlyleafLib.MediaFramework.MediaRemuxer;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDecoder;

public unsafe class VideoDecoder : DecoderBase
{
    public ConcurrentQueue<VideoFrame>
                            Frames              { get; protected set; } = new ConcurrentQueue<VideoFrame>();
    public Renderer         Renderer            { get; private set; }
    public bool             VideoAccelerated    { get; internal set; }
    public bool             ZeroCopy            { get; internal set; }

    public VideoStream      VideoStream         => (VideoStream) Stream;

    public long             StartTime           { get; internal set; } = AV_NOPTS_VALUE;
    public long             StartRecordTime     { get; internal set; } = AV_NOPTS_VALUE;

    const AVPixelFormat     PIX_FMT_HWACCEL     = AVPixelFormat.D3d11;
    const SwsFlags          SCALING_HQ          = SwsFlags.AccurateRnd | SwsFlags.Bitexact | SwsFlags.Lanczos | SwsFlags.FullChrHInt | SwsFlags.FullChrHInp;
    const SwsFlags          SCALING_LQ          = SwsFlags.Bicublin;

    internal SwsContext*    swsCtx;
    IntPtr                  swsBufferPtr;
    internal byte_ptrArray4 swsData;
    internal int_array4     swsLineSize;

    internal bool           swFallback;
    internal bool           keyPacketRequired;
    internal long           lastFixedPts;

    bool                    checkExtraFrames; // DecodeFrameNext

    // Reverse Playback
    ConcurrentStack<List<IntPtr>>
                            curReverseVideoStack    = new();
    List<IntPtr>            curReverseVideoPackets  = new();
    List<VideoFrame>        curReverseVideoFrames   = new();
    int                     curReversePacketPos     = 0;

    // Drop frames if FPS is higher than allowed
    int                     curSpeedFrame           = 9999; // don't skip first frame (on start/after seek-flush)
    double                  skipSpeedFrames         = 0;

    public VideoDecoder(Config config, int uniqueId = -1) : base(config, uniqueId)
        => getHWformat = new AVCodecContext_get_format(get_format);

    protected override void OnSpeedChanged(double value)
    {
        if (VideoStream == null) return;
        speed = value;
        skipSpeedFrames = speed * VideoStream.FPS / Config.Video.MaxOutputFps;
    }

    /// <summary>
    /// Prevents to get the first frame after seek/flush
    /// </summary>
    public void ResetSpeedFrame()
        => curSpeedFrame = 0;

    public void CreateRenderer() // TBR: It should be in the constructor but DecoderContext will not work with null VideoDecoder for AudioOnly
    {
        if (Renderer == null)
            Renderer = new Renderer(this, IntPtr.Zero, UniqueId);
        else if (Renderer.Disposed)
            Renderer.Initialize();
    }
    public void DestroyRenderer() => Renderer?.Dispose();
    public void CreateSwapChain(IntPtr handle)
    {
        CreateRenderer();
        Renderer.InitializeSwapChain(handle);
    }
    public void CreateSwapChain(Action<IDXGISwapChain2> swapChainWinUIClbk)
    {
        Renderer.SwapChainWinUIClbk = swapChainWinUIClbk;
        if (Renderer.SwapChainWinUIClbk != null)
            Renderer.InitializeWinUISwapChain();

    }
    public void DestroySwapChain() => Renderer?.DisposeSwapChain();

    #region Video Acceleration (Should be disposed seperately)
    const int               AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01;
    const AVHWDeviceType    HW_DEVICE = AVHWDeviceType.D3d11va;

    internal ID3D11Texture2D
                            textureFFmpeg;
    AVCodecContext_get_format
                            getHWformat;
    AVBufferRef*            hwframes;
    AVBufferRef*            hw_device_ctx;

    internal static bool CheckCodecSupport(AVCodec* codec)
    {
        for (int i = 0; ; i++)
        {
            var config = avcodec_get_hw_config(codec, i);
            if (config == null) break;
            if ((config->methods & AVCodecHwConfigMethod.HwDeviceCtx) == 0 || config->pix_fmt == AVPixelFormat.None) continue;

            if (config->device_type == HW_DEVICE && config->pix_fmt == PIX_FMT_HWACCEL) return true;
        }

        return false;
    }
    internal int InitVA()
    {
        int ret;
        AVHWDeviceContext*      device_ctx;
        AVD3D11VADeviceContext* d3d11va_device_ctx;

        if (Renderer.Device == null || hw_device_ctx != null) return -1;

        hw_device_ctx  = av_hwdevice_ctx_alloc(HW_DEVICE);

        device_ctx          = (AVHWDeviceContext*) hw_device_ctx->data;
        d3d11va_device_ctx  = (AVD3D11VADeviceContext*) device_ctx->hwctx;
        d3d11va_device_ctx->device
                            = (Flyleaf.FFmpeg.ID3D11Device*) Renderer.Device.NativePointer;

        ret = av_hwdevice_ctx_init(hw_device_ctx);
        if (ret != 0)
        {
            Log.Error($"VA Failed - {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

            fixed(AVBufferRef** ptr = &hw_device_ctx)
                av_buffer_unref(ptr);

            hw_device_ctx = null;
        }

        Renderer.Device.AddRef(); // Important to give another reference for FFmpeg so we can dispose without issues

        return ret;
    }

    private AVPixelFormat get_format(AVCodecContext* avctx, AVPixelFormat* pix_fmts)
    {
        if (CanDebug)
        {
            Log.Debug($"Codec profile '{VideoStream.Codec} {avcodec_profile_name(codecCtx->codec_id, codecCtx->profile)}'");

            if (CanTrace)
            {
                var save = pix_fmts;
                while (*pix_fmts != AVPixelFormat.None)
                {
                    Log.Trace($"{*pix_fmts}");
                    pix_fmts++;
                }
                pix_fmts = save;
            }
        }

        bool foundHWformat = false;

        while (*pix_fmts != AVPixelFormat.None)
        {
            if ((*pix_fmts) == PIX_FMT_HWACCEL)
            {
                foundHWformat = true;
                break;
            }

            pix_fmts++;
        }

        int ret = ShouldAllocateNew();

        if (foundHWformat && ret == 0)
        {
            if (codecCtx->hw_frames_ctx == null && hwframes != null)
                codecCtx->hw_frames_ctx = av_buffer_ref(hwframes);

            return PIX_FMT_HWACCEL;
        }

        lock (lockCodecCtx)
        {
            if (!foundHWformat || !VideoAccelerated || AllocateHWFrames() != 0)
            {
                if (CanWarn)
                    Log.Warn("HW format not found. Fallback to sw format");

                swFallback = true;
                return avcodec_default_get_format(avctx, pix_fmts);
            }

            if (CanDebug)
                Log.Debug("HW frame allocation completed");

            // TBR: Catch codec changed on live streams (check codec/profiles and check even on sw frames)
            if (ret == 2)
            {
                Log.Warn($"Codec changed {VideoStream.CodecID} {VideoStream.Width}x{VideoStream.Height} => {codecCtx->codec_id} {codecCtx->width}x{codecCtx->height}");
                filledFromCodec = false;
            }

            return PIX_FMT_HWACCEL;
        }
    }
    private int ShouldAllocateNew() // 0: No, 1: Yes, 2: Yes+Codec Changed
    {
        if (hwframes == null)
            return 1;

        AVHWFramesContext* t2 = (AVHWFramesContext*) hwframes->data;

        if (codecCtx->coded_width != t2->width)
            return 2;

        if (codecCtx->coded_height != t2->height)
            return 2;

        // TBR: Codec changed (seems ffmpeg changes codecCtx by itself
        //if (codecCtx->codec_id != VideoStream.CodecID)
        //    return 2;

        //var fmt = codecCtx->sw_pix_fmt == (AVPixelFormat)AV_PIX_FMT_YUV420P10LE ? (AVPixelFormat)AV_PIX_FMT_P010LE : (codecCtx->sw_pix_fmt == (AVPixelFormat)AV_PIX_FMT_P010BE ? (AVPixelFormat)AV_PIX_FMT_P010BE : AVPixelFormat.AV_PIX_FMT_NV12);
        //if (fmt != t2->sw_format)
        //    return 2;

        return 0;
    }

    private int AllocateHWFrames()
    {
        if (hwframes != null)
            fixed(AVBufferRef** ptr = &hwframes)
                av_buffer_unref(ptr);

        hwframes = null;

        if (codecCtx->hw_frames_ctx != null)
            av_buffer_unref(&codecCtx->hw_frames_ctx);

        if (avcodec_get_hw_frames_parameters(codecCtx, codecCtx->hw_device_ctx, PIX_FMT_HWACCEL, &codecCtx->hw_frames_ctx) != 0)
            return -1;

        AVHWFramesContext* hw_frames_ctx = (AVHWFramesContext*)codecCtx->hw_frames_ctx->data;
        //hw_frames_ctx->initial_pool_size += Config.Decoder.MaxVideoFrames; // TBR: Texture 2D Array seems to have up limit to 128 (total=17+MaxVideoFrames)? (should use extra hw frames instead**)

        AVD3D11VAFramesContext *va_frames_ctx = (AVD3D11VAFramesContext *)hw_frames_ctx->hwctx;
        va_frames_ctx->BindFlags  |= (uint)BindFlags.Decoder | (uint)BindFlags.ShaderResource;

        hwframes = av_buffer_ref(codecCtx->hw_frames_ctx);

        int ret = av_hwframe_ctx_init(codecCtx->hw_frames_ctx);
        if (ret == 0)
        {
            lock (Renderer.lockDevice)
            {
                textureFFmpeg   = new ID3D11Texture2D((IntPtr) va_frames_ctx->texture);
                ZeroCopy = Config.Decoder.ZeroCopy == FlyleafLib.ZeroCopy.Enabled || (Config.Decoder.ZeroCopy == FlyleafLib.ZeroCopy.Auto && codecCtx->width == textureFFmpeg.Description.Width && codecCtx->height == textureFFmpeg.Description.Height);
                filledFromCodec = false;
            }
        }

        return ret;
    }
    internal void RecalculateZeroCopy()
    {
        lock (Renderer.lockDevice)
        {
            bool save = ZeroCopy;
            ZeroCopy = VideoAccelerated && (Config.Decoder.ZeroCopy == FlyleafLib.ZeroCopy.Enabled || (Config.Decoder.ZeroCopy == FlyleafLib.ZeroCopy.Auto && codecCtx->width == textureFFmpeg.Description.Width && codecCtx->height == textureFFmpeg.Description.Height));
            if (save != ZeroCopy)
            {
                Renderer?.ConfigPlanes();
                CodecChanged?.Invoke(this);
            }
        }
    }
    #endregion

    protected override int Setup(AVCodec* codec)
    {
        // Ensures we have a renderer (no swap chain is required)
        CreateRenderer();

        VideoAccelerated = false;

        if (!swFallback && Config.Video.VideoAcceleration && Renderer.Device.FeatureLevel >= Vortice.Direct3D.FeatureLevel.Level_10_0)
        {
            if (CheckCodecSupport(codec))
            {
                if (InitVA() == 0)
                {
                    codecCtx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
                    VideoAccelerated = true;
                    Log.Debug("VA Success");
                }
            }
            else
                Log.Info($"VA {codec->id} not supported");
        }
        else
            Log.Debug("VA Disabled");

        // Can't get data from here?
        //var t1 = av_stream_get_side_data(VideoStream.AVStream, AVPacketSideDataType.AV_PKT_DATA_MASTERING_DISPLAY_METADATA, null);
        //var t2 = av_stream_get_side_data(VideoStream.AVStream, AVPacketSideDataType.AV_PKT_DATA_CONTENT_LIGHT_LEVEL, null);

        // TBR: during swFallback (keyFrameRequiredPacket should not reset, currenlty saved in SWFallback)
        keyPacketRequired = false; // allow no key packet after open (lot of videos missing this)
        ZeroCopy        = false;
        filledFromCodec = false;

        lastFixedPts    = 0; // TBR: might need to set this to first known pts/dts

        if (VideoAccelerated)
        {
            codecCtx->thread_count      = 1;
            codecCtx->hwaccel_flags    |= HWAccelFlags.IgnoreLevel;
            if (Config.Decoder.AllowProfileMismatch)
                codecCtx->hwaccel_flags|= HWAccelFlags.AllowProfileMismatch;

            codecCtx->get_format        = getHWformat;
            codecCtx->extra_hw_frames   = Config.Decoder.MaxVideoFrames;
        }
        else
            codecCtx->thread_count = Math.Min(Config.Decoder.VideoThreads, codecCtx->codec_id == AVCodecID.Hevc ? 32 : 16);

        return 0;
    }
    internal bool SetupSws()
    {
        Marshal.FreeHGlobal(swsBufferPtr);
        var fmt         = AVPixelFormat.Rgba;
        swsData         = new byte_ptrArray4();
        swsLineSize     = new int_array4();
        int outBufferSize
                        = av_image_get_buffer_size(fmt, codecCtx->width, codecCtx->height, 1);
        swsBufferPtr    = Marshal.AllocHGlobal(outBufferSize);
        av_image_fill_arrays(ref swsData, ref swsLineSize, (byte*) swsBufferPtr, fmt, codecCtx->width, codecCtx->height, 1);
        swsCtx          = sws_getContext(codecCtx->coded_width, codecCtx->coded_height, codecCtx->pix_fmt, codecCtx->width, codecCtx->height, fmt, Config.Video.SwsHighQuality ? SCALING_HQ : SCALING_LQ, null, null, null);

        if (swsCtx == null)
        {
            Log.Error($"Failed to allocate SwsContext");
            return false;
        }

        return true;
    }
    internal void Flush()
    {
        lock (lockActions)
            lock (lockCodecCtx)
            {
                if (Disposed) return;

                if (Status == Status.Ended)
                    Status = Status.Stopped;
                else if (Status == Status.Draining)
                    Status = Status.Stopping;

                DisposeFrames();
                avcodec_flush_buffers(codecCtx);

                keyPacketRequired
                                = true;
                StartTime       = AV_NOPTS_VALUE;
                curSpeedFrame   = 9999;
            }
    }

    protected override void RunInternal()
    {
        if (demuxer.IsReversePlayback)
        {
            RunInternalReverse();
            return;
        }

        int ret = 0;
        int allowedErrors = Config.Decoder.MaxErrors;
        int sleepMs = Config.Decoder.MaxVideoFrames > 2 && Config.Player.MaxLatency == 0 ? 10 : 2;
        AVPacket *packet;

        do
        {
            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxVideoFrames)
            {
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxVideoFrames && Status == Status.QueueFull)
                    Thread.Sleep(sleepMs);

                lock (lockStatus)
                {
                    if (Status != Status.QueueFull) break;
                    Status = Status.Running;
                }
            }

            // While Packets Queue Empty (Drain | Quit if Demuxer stopped | Wait until we get packets)
            if (demuxer.VideoPackets.Count == 0)
            {
                CriticalArea = true;

                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueEmpty;

                while (demuxer.VideoPackets.Count == 0 && Status == Status.QueueEmpty)
                {
                    if (demuxer.Status == Status.Ended)
                    {
                        lock (lockStatus)
                        {
                            // TODO: let the demuxer push the draining packet
                            Log.Debug("Draining");
                            Status = Status.Draining;
                            var drainPacket = av_packet_alloc();
                            drainPacket->data = null;
                            drainPacket->size = 0;
                            demuxer.VideoPackets.Enqueue(drainPacket);
                        }

                        break;
                    }
                    else if (!demuxer.IsRunning)
                    {
                        if (CanDebug) Log.Debug($"Demuxer is not running [Demuxer Status: {demuxer.Status}]");

                        int retries = 5;

                        while (retries > 0)
                        {
                            retries--;
                            Thread.Sleep(10);
                            if (demuxer.IsRunning) break;
                        }

                        lock (demuxer.lockStatus)
                        lock (lockStatus)
                        {
                            if (demuxer.Status == Status.Pausing || demuxer.Status == Status.Paused)
                                Status = Status.Pausing;
                            else if (demuxer.Status != Status.Ended)
                                Status = Status.Stopping;
                            else
                                continue;
                        }

                        break;
                    }

                    Thread.Sleep(sleepMs);
                }

                lock (lockStatus)
                {
                    CriticalArea = false;
                    if (Status != Status.QueueEmpty && Status != Status.Draining) break;
                    if (Status != Status.Draining) Status = Status.Running;
                }
            }

            lock (lockCodecCtx)
            {
                if (Status == Status.Stopped)
                    continue;

                packet = demuxer.VideoPackets.Dequeue();

                if (packet == null)
                    continue;

                if (isRecording)
                {
                    if (!recKeyPacketRequired && (packet->flags & PktFlags.Key) != 0)
                    {
                        recKeyPacketRequired = true;
                        StartRecordTime = (long)(packet->pts * VideoStream.Timebase) - demuxer.StartTime;
                    }

                    if (recKeyPacketRequired)
                        curRecorder.Write(av_packet_clone(packet));
                }

                if (keyPacketRequired)
                {
                    if (packet->flags.HasFlag(PktFlags.Key))
                        keyPacketRequired = false;
                    else
                    {
                        if (CanWarn) Log.Warn("Ignoring non-key packet");
                        av_packet_unref(packet);
                        continue;
                    }
                }

                // TBR: AVERROR(EAGAIN) means avcodec_receive_frame but after resend the same packet
                ret = avcodec_send_packet(codecCtx, packet);

                if (swFallback) // Should use 'global' packet to reset it in get_format (same packet should use also from DecoderContext)
                {
                    SWFallback();
                    ret = avcodec_send_packet(codecCtx, packet);
                }

                if (ret != 0 && ret != AVERROR(EAGAIN))
                {
                    // TBR: Possible check for VA failed here (normally this will happen during get_format)
                    av_packet_free(&packet);

                    if (ret == AVERROR_EOF)
                    {
                        if (demuxer.VideoPackets.Count > 0) { avcodec_flush_buffers(codecCtx); continue; } // TBR: Happens on HLS while switching video streams
                        Status = Status.Ended;
                        break;
                    }
                    else
                    {
                        allowedErrors--;
                        if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                        if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                        continue;
                    }
                }

                while (true)
                {
                    ret = avcodec_receive_frame(codecCtx, frame);
                    if (ret != 0) { av_frame_unref(frame); break; }

                    // GetFormat checks already for this but only for hardware accelerated (should also check for codec/fps* and possible reset sws if required)
                    // Might use AVERROR_INPUT_CHANGED to let ffmpeg check for those (requires a flag to be set*)
                    if (frame->height != VideoStream.Height || frame->width != VideoStream.Width)
                    {
                        // THIS IS Wrong and can cause filledFromCodec all the time. comparing frame<->videostream dimensions but we update the videostream from codecparam dimensions (which we pass from codecCtx w/h)
                        // Related with display dimensions / coded dimensions / frame-crop dimensions (and apply_cropping) - it could happen when frame->crop... are not 0
                        Log.Warn($"Codec changed {VideoStream.CodecID} {VideoStream.Width}x{VideoStream.Height} => {codecCtx->codec_id} {frame->width}x{frame->height}");
                        filledFromCodec = false;
                    }

                    if (frame->best_effort_timestamp != AV_NOPTS_VALUE)
                        frame->pts = frame->best_effort_timestamp;

                    else if (frame->pts == AV_NOPTS_VALUE)
                    {
                        if (!VideoStream.FixTimestamps && VideoStream.Duration > TimeSpan.FromSeconds(1).Ticks)
                        {
                            // TBR: it is possible to have a single frame / image with no dts/pts which actually means pts = 0 ? (ticket_3449.264) - GenPts will not affect it
                            // TBR: first frame might no have dts/pts which probably means pts = 0 (and not start time!)
                            av_frame_unref(frame);
                            continue;
                        }

                        // Create timestamps for h264/hevc raw streams (Needs also to handle this with the remuxer / no recording currently supported!)
                        frame->pts = lastFixedPts + VideoStream.StartTimePts;
                        lastFixedPts += av_rescale_q(VideoStream.FrameDuration / 10, Engine.FFmpeg.AV_TIMEBASE_Q, VideoStream.AVStream->time_base);
                    }

                    if (StartTime == NoTs)
                        StartTime = (long)(frame->pts * VideoStream.Timebase) - demuxer.StartTime;

                    if (!filledFromCodec) // Ensures we have a proper frame before filling from codec
                    {
                        ret = FillFromCodec(frame);
                        if (ret == -1234)
                        {
                            Status = Status.Stopping;
                            break;
                        }
                    }

                    if (skipSpeedFrames > 1)
                    {
                        curSpeedFrame++;
                        if (curSpeedFrame < skipSpeedFrames)
                        {
                            av_frame_unref(frame);
                            continue;
                        }
                        curSpeedFrame = 0;
                    }

                    var mFrame = Renderer.FillPlanes(frame);
                    if (mFrame != null) Frames.Enqueue(mFrame); // TBR: Does not respect Config.Decoder.MaxVideoFrames

                    if (!Config.Video.PresentFlags.HasFlag(PresentFlags.DoNotWait) && Frames.Count > 2)
                        Thread.Sleep(10);
                }

                av_packet_free(&packet);
            }

        } while (Status == Status.Running);

        checkExtraFrames = true;

        if (isRecording) { StopRecording(); recCompleted(MediaType.Video); }

        if (Status == Status.Draining) Status = Status.Ended;
    }

    internal int FillFromCodec(AVFrame* frame)
    {
        lock (Renderer.lockDevice)
        {
            int ret = 0;

            filledFromCodec = true;

            avcodec_parameters_from_context(Stream.AVStream->codecpar, codecCtx);
            VideoStream.AVStream->time_base = codecCtx->pkt_timebase;
            VideoStream.Refresh(VideoAccelerated && codecCtx->sw_pix_fmt != AVPixelFormat.None ? codecCtx->sw_pix_fmt : codecCtx->pix_fmt, frame);

            if (!(VideoStream.FPS > 0)) // NaN
            {
                VideoStream.FPS             = av_q2d(codecCtx->framerate) > 0 ? av_q2d(codecCtx->framerate) : 0;
                VideoStream.FrameDuration   = VideoStream.FPS > 0 ? (long) (10000000 / VideoStream.FPS) : 0;
                if (VideoStream.FrameDuration > 0)
                    VideoStream.Demuxer.VideoPackets.frameDuration = VideoStream.FrameDuration;
            }

            skipSpeedFrames = speed * VideoStream.FPS / Config.Video.MaxOutputFps;
            CodecChanged?.Invoke(this);

            if (VideoStream.PixelFormat == AVPixelFormat.None || !Renderer.ConfigPlanes())
            {
                Log.Error("[Pixel Format] Unknown");
                return -1234;
            }

            return ret;
        }
    }

    internal string SWFallback()
    {
        lock (Renderer.lockDevice)
        {
            string ret;

            DisposeInternal();
            if (codecCtx != null)
                fixed (AVCodecContext** ptr = &codecCtx)
                    avcodec_free_context(ptr);

            codecCtx        = null;
            swFallback      = true;
            bool oldKeyFrameRequiredPacket
                            = keyPacketRequired;
            ret = Open2(Stream, null, false); // TBR:  Dispose() on failure could cause a deadlock
            keyPacketRequired
                            = oldKeyFrameRequiredPacket;
            swFallback      = false;
            filledFromCodec = false;

            return ret;
        }
    }

    private void RunInternalReverse()
    {
        // Bug with B-frames, we should not remove the ref packets (we miss frames each time we restart decoding the gop)

        int ret = 0;
        int allowedErrors = Config.Decoder.MaxErrors;
        AVPacket *packet;

        do
        {
            // While Packets Queue Empty (Drain | Quit if Demuxer stopped | Wait until we get packets)
            if (demuxer.VideoPacketsReverse.IsEmpty && curReverseVideoStack.IsEmpty && curReverseVideoPackets.Count == 0)
            {
                CriticalArea = true;

                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueEmpty;

                while (demuxer.VideoPacketsReverse.IsEmpty && Status == Status.QueueEmpty)
                {
                    if (demuxer.Status == Status.Ended) // TODO
                    {
                        lock (lockStatus) Status = Status.Ended;

                        break;
                    }
                    else if (!demuxer.IsRunning)
                    {
                        if (CanDebug) Log.Debug($"Demuxer is not running [Demuxer Status: {demuxer.Status}]");

                        int retries = 5;

                        while (retries > 0)
                        {
                            retries--;
                            Thread.Sleep(10);
                            if (demuxer.IsRunning) break;
                        }

                        lock (demuxer.lockStatus)
                        lock (lockStatus)
                        {
                            if (demuxer.Status == Status.Pausing || demuxer.Status == Status.Paused)
                                Status = Status.Pausing;
                            else if (demuxer.Status != Status.Ended)
                                Status = Status.Stopping;
                            else
                                continue;
                        }

                        break;
                    }

                    Thread.Sleep(20);
                }

                lock (lockStatus)
                {
                    CriticalArea = false;
                    if (Status != Status.QueueEmpty) break;
                    Status = Status.Running;
                }
            }

            if (curReverseVideoPackets.Count == 0)
            {
                if (curReverseVideoStack.IsEmpty)
                    demuxer.VideoPacketsReverse.TryDequeue(out curReverseVideoStack);

                curReverseVideoStack.TryPop(out curReverseVideoPackets);
                curReversePacketPos = 0;
            }

            while (curReverseVideoPackets.Count > 0 && Status == Status.Running)
            {
                // Wait until Queue not Full or Stopped
                if (Frames.Count + curReverseVideoFrames.Count >= Config.Decoder.MaxVideoFrames)
                {
                    lock (lockStatus)
                        if (Status == Status.Running) Status = Status.QueueFull;

                    while (Frames.Count + curReverseVideoFrames.Count >= Config.Decoder.MaxVideoFrames && Status == Status.QueueFull) Thread.Sleep(20);

                    lock (lockStatus)
                    {
                        if (Status != Status.QueueFull) break;
                        Status = Status.Running;
                    }
                }

                lock (lockCodecCtx)
                {
                    if (keyPacketRequired == true)
                    {
                        keyPacketRequired = false;
                        curReversePacketPos = 0;
                        break;
                    }

                    packet = (AVPacket*)curReverseVideoPackets[curReversePacketPos++];
                    ret = avcodec_send_packet(codecCtx, packet);

                    if (ret != 0 && ret != AVERROR(EAGAIN))
                    {
                        if (ret == AVERROR_EOF) { Status = Status.Ended; break; }

                        if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                        allowedErrors--;
                        if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                        for (int i=curReverseVideoPackets.Count-1; i>=curReversePacketPos-1; i--)
                        {
                            packet = (AVPacket*)curReverseVideoPackets[i];
                            av_packet_free(&packet);
                            curReverseVideoPackets[curReversePacketPos - 1] = IntPtr.Zero;
                            curReverseVideoPackets.RemoveAt(i);
                        }

                        avcodec_flush_buffers(codecCtx);
                        curReversePacketPos = 0;

                        for (int i=curReverseVideoFrames.Count -1; i>=0; i--)
                            Frames.Enqueue(curReverseVideoFrames[i]);

                        curReverseVideoFrames.Clear();

                        continue;
                    }

                    while (true)
                    {
                        ret = avcodec_receive_frame(codecCtx, frame);
                        if (ret != 0) { av_frame_unref(frame); break; }

                        if (frame->best_effort_timestamp != AV_NOPTS_VALUE)
                            frame->pts = frame->best_effort_timestamp;
                        else if (frame->pts == AV_NOPTS_VALUE)
                            { av_frame_unref(frame); continue; }

                        bool shouldProcess = curReverseVideoPackets.Count - curReversePacketPos < Config.Decoder.MaxVideoFrames;

                        if (shouldProcess)
                        {
                            av_packet_free(&packet);
                            curReverseVideoPackets[curReversePacketPos - 1] = IntPtr.Zero;
                            var mFrame = Renderer.FillPlanes(frame);
                            if (mFrame != null) curReverseVideoFrames.Add(mFrame);
                        }
                        else
                            av_frame_unref(frame);
                    }

                    if (curReversePacketPos == curReverseVideoPackets.Count)
                    {
                        curReverseVideoPackets.RemoveRange(Math.Max(0, curReverseVideoPackets.Count - Config.Decoder.MaxVideoFrames), Math.Min(curReverseVideoPackets.Count, Config.Decoder.MaxVideoFrames) );
                        avcodec_flush_buffers(codecCtx);
                        curReversePacketPos = 0;

                        for (int i=curReverseVideoFrames.Count -1; i>=0; i--)
                            Frames.Enqueue(curReverseVideoFrames[i]);

                        curReverseVideoFrames.Clear();

                        break; // force recheck for max queues etc...
                    }

                } // Lock CodecCtx

                // Import Sleep required to prevent delay during Renderer.Present for waitable swap chains
                if (!Config.Video.PresentFlags.HasFlag(PresentFlags.DoNotWait) && Frames.Count > 2)
                    Thread.Sleep(10);

            } // while curReverseVideoPackets.Count > 0

        } while (Status == Status.Running);

        if (Status != Status.Pausing && Status != Status.Paused)
            curReversePacketPos = 0;
    }

    public void RefreshMaxVideoFrames()
    {
        lock (lockActions)
        {
            if (VideoStream == null)
                return;

            bool wasRunning = IsRunning;

            var stream = Stream;

            Dispose();
            Open(stream);

            if (wasRunning)
                Start();
        }
    }

    public int GetFrameNumber(long timestamp)
    {
        // Incoming timestamps are zero-base from demuxer start time (not from video stream start time)
        timestamp -= VideoStream.StartTime - demuxer.StartTime;

        if (timestamp < 1)
            return 0;

        // offset 2ms
        return (int) ((timestamp + 20000) / VideoStream.FrameDuration);
    }

    /// <summary>
    /// Performs accurate seeking to the requested VideoFrame and returns it
    /// </summary>
    /// <param name="index">Zero based frame index</param>
    /// <returns>The requested VideoFrame or null on failure</returns>
    public VideoFrame GetFrame(int index)
    {
        int ret;

        // Calculation of FrameX timestamp (based on fps/avgFrameDuration) | offset 2ms
        long frameTimestamp = VideoStream.StartTime + (index * VideoStream.FrameDuration) - 20000;
        //Log.Debug($"Searching for {Utils.TicksToTime(frameTimestamp)}");

        demuxer.Pause();
        Pause();

        // TBR
        //if (demuxer.FormatContext->pb != null)
        //    avio_flush(demuxer.FormatContext->pb);
        //avformat_flush(demuxer.FormatContext);

        // Seeking at frameTimestamp or previous I/Key frame and flushing codec | Temp fix (max I/distance 3sec) for ffmpeg bug that fails to seek on keyframe with HEVC
        // More issues with mpegts seeking backwards (those should be used also in the reverse playback in the demuxer)
        demuxer.Interrupter.SeekRequest();
        ret = codecCtx->codec_id == AVCodecID.Hevc|| (demuxer.FormatContext->iformat != null) // TBR: this is on FFInputFormat now -> && demuxer.FormatContext->iformat-> read_seek.Pointer == IntPtr.Zero)
            ? av_seek_frame(demuxer.FormatContext, -1, Math.Max(0, frameTimestamp - Config.Player.SeekGetFrameFixMargin) / 10, SeekFlags.Any)
            : av_seek_frame(demuxer.FormatContext, -1, frameTimestamp / 10, SeekFlags.Frame | SeekFlags.Backward);

        demuxer.DisposePackets();

        if (demuxer.Status == Status.Ended) demuxer.Status = Status.Stopped;
        if (ret < 0) return null; // handle seek error
        Flush();
        checkExtraFrames = false;

        while (DecodeFrameNext() == 0)
        {
            // Skip frames before our actual requested frame
            if ((long)(frame->pts * VideoStream.Timebase) < frameTimestamp)
            {
                //Log.Debug($"[Skip] [pts: {frame->pts}] [time: {Utils.TicksToTime((long)(frame->pts * VideoStream.Timebase))}] | [fltime: {Utils.TicksToTime(((long)(frame->pts * VideoStream.Timebase) - demuxer.StartTime))}]");
                av_frame_unref(frame);
                continue;
            }

            //Log.Debug($"[Found] [pts: {frame->pts}] [time: {Utils.TicksToTime((long)(frame->pts * VideoStream.Timebase))}] | {Utils.TicksToTime(VideoStream.StartTime + (index * VideoStream.FrameDuration))} | [fltime: {Utils.TicksToTime(((long)(frame->pts * VideoStream.Timebase) - demuxer.StartTime))}]");
            return Renderer.FillPlanes(frame);
        }

        return null;
    }

    /// <summary>
    /// Gets next VideoFrame (Decoder/Demuxer must not be running)
    /// </summary>
    /// <returns>The next VideoFrame</returns>
    public VideoFrame GetFrameNext()
        => DecodeFrameNext() == 0 ? Renderer.FillPlanes(frame) : null;

    /// <summary>
    /// Pushes the decoder to the next available VideoFrame (Decoder/Demuxer must not be running)
    /// </summary>
    /// <returns></returns>
    public int DecodeFrameNext()
    {
        int ret;
        int allowedErrors = Config.Decoder.MaxErrors;

        if (checkExtraFrames)
        {
            if (Status == Status.Ended)
                return AVERROR_EOF;

            if (DecodeFrameNextInternal() == 0)
                return 0;

            if (Demuxer.Status == Status.Ended && demuxer.VideoPackets.Count == 0 && Frames.IsEmpty)
            {
                Stop();
                Status = Status.Ended;
                return AVERROR_EOF;
            }

            checkExtraFrames = false;
        }

        while (true)
        {
            ret = demuxer.GetNextVideoPacket();
            if (ret != 0)
            {
                if (demuxer.Status != Status.Ended)
                    return ret;

                // Drain
                ret = avcodec_send_packet(codecCtx, demuxer.packet);
                av_packet_unref(demuxer.packet);

                if (ret != 0)
                    return AVERROR_EOF;

                checkExtraFrames = true;
                return DecodeFrameNext();
            }

            if (keyPacketRequired)
            {
                if (demuxer.packet->flags.HasFlag(PktFlags.Key))
                    keyPacketRequired = false;
                else
                {
                    if (CanWarn) Log.Warn("Ignoring non-key packet");
                    av_packet_unref(demuxer.packet);
                    continue;
                }
            }

            ret = avcodec_send_packet(codecCtx, demuxer.packet);

            if (swFallback) // Should use 'global' packet to reset it in get_format (same packet should use also from DecoderContext)
            {
                SWFallback();
                ret = avcodec_send_packet(codecCtx, demuxer.packet);
            }

            av_packet_unref(demuxer.packet);

            if (ret != 0 && ret != AVERROR(EAGAIN))
            {
                if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                if (allowedErrors-- < 1)
                    { Log.Error("Too many errors!"); return ret; }

                continue;
            }

            if (DecodeFrameNextInternal() == 0)
            {
                checkExtraFrames = true;
                return 0;
            }
        }

    }
    private int DecodeFrameNextInternal()
    {
        int ret = avcodec_receive_frame(codecCtx, frame);
        if (ret != 0) { av_frame_unref(frame); return ret; }

        if (frame->best_effort_timestamp != AV_NOPTS_VALUE)
            frame->pts = frame->best_effort_timestamp;

        else if (frame->pts == AV_NOPTS_VALUE)
        {
            if (!VideoStream.FixTimestamps)
            {
                av_frame_unref(frame);

                return DecodeFrameNextInternal();
            }

            frame->pts = lastFixedPts + VideoStream.StartTimePts;
            lastFixedPts += av_rescale_q(VideoStream.FrameDuration / 10, Engine.FFmpeg.AV_TIMEBASE_Q, VideoStream.AVStream->time_base);
        }

        if (StartTime == NoTs)
            StartTime = (long)(frame->pts * VideoStream.Timebase) - demuxer.StartTime;

        if (!filledFromCodec) // Ensures we have a proper frame before filling from codec
        {
            ret = FillFromCodec(frame);
            if (ret == -1234)
                return -1;
        }

        return 0;
    }

    #region Dispose
    public void DisposeFrames()
    {
        while (!Frames.IsEmpty)
        {
            Frames.TryDequeue(out var frame);
            DisposeFrame(frame);
        }

        DisposeFramesReverse();
    }
    private void DisposeFramesReverse()
    {
        while (!curReverseVideoStack.IsEmpty)
        {
            curReverseVideoStack.TryPop(out var t2);
            for (int i = 0; i<t2.Count; i++)
            {
                if (t2[i] == IntPtr.Zero) continue;
                AVPacket* packet = (AVPacket*)t2[i];
                av_packet_free(&packet);
            }
        }

        for (int i = 0; i<curReverseVideoPackets.Count; i++)
        {
            if (curReverseVideoPackets[i] == IntPtr.Zero) continue;
            AVPacket* packet = (AVPacket*)curReverseVideoPackets[i];
            av_packet_free(&packet);
        }

        curReverseVideoPackets.Clear();

        for (int i=0; i<curReverseVideoFrames.Count; i++)
            DisposeFrame(curReverseVideoFrames[i]);

        curReverseVideoFrames.Clear();
    }
    public static void DisposeFrame(VideoFrame frame)
    {
        if (frame == null)
            return;

        if (frame.textures != null)
        {
            for (int i=0; i<frame.textures.Length; i++)
                frame.textures[i].Dispose();

            frame.textures = null;
        }

        if (frame.srvs != null)
        {
            for (int i=0; i<frame.srvs.Length; i++)
                frame.srvs[i].Dispose();

            frame.srvs = null;
        }

        if (frame.avFrame != null)
            fixed(AVFrame** ptr = &frame.avFrame)
            av_frame_free(ptr);
    }
    protected override void DisposeInternal()
    {
        lock (lockCodecCtx)
        {
            DisposeFrames();

            if (hwframes != null)
                fixed(AVBufferRef** ptr = &hwframes)
                    av_buffer_unref(ptr);

            if (hw_device_ctx != null)
                fixed(AVBufferRef** ptr = &hw_device_ctx)
                    av_buffer_unref(ptr);

            if (swsCtx != null)
                sws_freeContext(swsCtx);

            hwframes    = null;
            hw_device_ctx
                        = null;
            swsCtx      = null;
            StartTime   = AV_NOPTS_VALUE;
            swFallback  = false;
            curSpeedFrame= 9999;
        }
    }
    #endregion

    #region Recording
    internal Action<MediaType> recCompleted;
    Remuxer curRecorder;
    bool recKeyPacketRequired;
    internal bool isRecording;

    internal void StartRecording(Remuxer remuxer)
    {
        if (Disposed || isRecording) return;

        StartRecordTime     = AV_NOPTS_VALUE;
        curRecorder         = remuxer;
        recKeyPacketRequired= false;
        isRecording         = true;
    }
    internal void StopRecording() => isRecording = false;
    #endregion

    #region TODO Decoder Profiles

    /* Use the same as FFmpeg
     * https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/dxva2.c
     * https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.h
     */

    //internal enum DecoderProfiles
    //{
    //    DXVA_ModeMPEG2and1_VLD,
    //    DXVA_ModeMPEG1_VLD,
    //    DXVA2_ModeMPEG2_VLD,
    //    DXVA2_ModeMPEG2_IDCT,
    //    DXVA2_ModeMPEG2_MoComp,
    //    DXVA_ModeH264_A,
    //    DXVA_ModeH264_B,
    //    DXVA_ModeH264_C,
    //    DXVA_ModeH264_D,
    //    DXVA_ModeH264_E,
    //    DXVA_ModeH264_F,
    //    DXVA_ModeH264_VLD_Stereo_Progressive_NoFGT,
    //    DXVA_ModeH264_VLD_Stereo_NoFGT,
    //    DXVA_ModeH264_VLD_Multiview_NoFGT,
    //    DXVA_ModeWMV8_A,
    //    DXVA_ModeWMV8_B,
    //    DXVA_ModeWMV9_A,
    //    DXVA_ModeWMV9_B,
    //    DXVA_ModeWMV9_C,
    //    DXVA_ModeVC1_A,
    //    DXVA_ModeVC1_B,
    //    DXVA_ModeVC1_C,
    //    DXVA_ModeVC1_D,
    //    DXVA_ModeVC1_D2010,
    //    DXVA_ModeMPEG4pt2_VLD_Simple,
    //    DXVA_ModeMPEG4pt2_VLD_AdvSimple_NoGMC,
    //    DXVA_ModeMPEG4pt2_VLD_AdvSimple_GMC,
    //    DXVA_ModeHEVC_VLD_Main,
    //    DXVA_ModeHEVC_VLD_Main10,
    //    DXVA_ModeVP8_VLD,
    //    DXVA_ModeVP9_VLD_Profile0,
    //    DXVA_ModeVP9_VLD_10bit_Profile2,
    //    DXVA_ModeMPEG1_A,
    //    DXVA_ModeMPEG2_A,
    //    DXVA_ModeMPEG2_B,
    //    DXVA_ModeMPEG2_C,
    //    DXVA_ModeMPEG2_D,
    //    DXVA_ModeH261_A,
    //    DXVA_ModeH261_B,
    //    DXVA_ModeH263_A,
    //    DXVA_ModeH263_B,
    //    DXVA_ModeH263_C,
    //    DXVA_ModeH263_D,
    //    DXVA_ModeH263_E,
    //    DXVA_ModeH263_F,
    //    DXVA_ModeH264_VLD_WithFMOASO_NoFGT,
    //    DXVA_ModeH264_VLD_Multiview,
    //    DXVADDI_Intel_ModeH264_A,
    //    DXVADDI_Intel_ModeH264_C,
    //    DXVA_Intel_H264_NoFGT_ClearVideo,
    //    DXVA_ModeH264_VLD_NoFGT_Flash,
    //    DXVA_Intel_VC1_ClearVideo,
    //    DXVA_Intel_VC1_ClearVideo_2,
    //    DXVA_nVidia_MPEG4_ASP,
    //    DXVA_ModeMPEG4pt2_VLD_AdvSimple_Avivo,
    //    DXVA_ModeHEVC_VLD_Main_Intel,
    //    DXVA_ModeHEVC_VLD_Main10_Intel,
    //    DXVA_ModeHEVC_VLD_Main12_Intel,
    //    DXVA_ModeHEVC_VLD_Main422_10_Intel,
    //    DXVA_ModeHEVC_VLD_Main422_12_Intel,
    //    DXVA_ModeHEVC_VLD_Main444_Intel,
    //    DXVA_ModeHEVC_VLD_Main444_10_Intel,
    //    DXVA_ModeHEVC_VLD_Main444_12_Intel,
    //    DXVA_ModeH264_VLD_SVC_Scalable_Baseline,
    //    DXVA_ModeH264_VLD_SVC_Restricted_Scalable_Baseline,
    //    DXVA_ModeH264_VLD_SVC_Scalable_High,
    //    DXVA_ModeH264_VLD_SVC_Restricted_Scalable_High_Progressive,
    //    DXVA_ModeVP9_VLD_Intel,
    //    DXVA_ModeAV1_VLD_Profile0,
    //    DXVA_ModeAV1_VLD_Profile1,
    //    DXVA_ModeAV1_VLD_Profile2,
    //    DXVA_ModeAV1_VLD_12bit_Profile2,
    //    DXVA_ModeAV1_VLD_12bit_Profile2_420
    //}
    //internal static Dictionary<Guid, DecoderProfiles> DXVADecoderProfiles = new()
    //{
    //    { new(0x86695f12, 0x340e, 0x4f04, 0x9f, 0xd3, 0x92, 0x53, 0xdd, 0x32, 0x74, 0x60), DecoderProfiles.DXVA_ModeMPEG2and1_VLD },
    //    { new(0x6f3ec719, 0x3735, 0x42cc, 0x80, 0x63, 0x65, 0xcc, 0x3c, 0xb3, 0x66, 0x16), DecoderProfiles.DXVA_ModeMPEG1_VLD },
    //    { new(0xee27417f, 0x5e28,0x4e65, 0xbe, 0xea, 0x1d, 0x26, 0xb5, 0x08, 0xad, 0xc9), DecoderProfiles.DXVA2_ModeMPEG2_VLD },
    //    { new(0xbf22ad00, 0x03ea,0x4690, 0x80, 0x77, 0x47, 0x33, 0x46, 0x20, 0x9b, 0x7e), DecoderProfiles.DXVA2_ModeMPEG2_IDCT },
    //    { new(0xe6a9f44b, 0x61b0,0x4563, 0x9e, 0xa4, 0x63, 0xd2, 0xa3, 0xc6, 0xfe, 0x66), DecoderProfiles.DXVA2_ModeMPEG2_MoComp },
    //    { new(0x1b81be64, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_A },
    //    { new(0x1b81be65, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_B },
    //    { new(0x1b81be66, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_C },
    //    { new(0x1b81be67, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_D },
    //    { new(0x1b81be68, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_E },
    //    { new(0x1b81be69, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH264_F },
    //    { new(0xd79be8da, 0x0cf1,0x4c81, 0xb8, 0x2a, 0x69, 0xa4, 0xe2, 0x36, 0xf4, 0x3d), DecoderProfiles.DXVA_ModeH264_VLD_Stereo_Progressive_NoFGT },
    //    { new(0xf9aaccbb, 0xc2b6,0x4cfc, 0x87, 0x79, 0x57, 0x07, 0xb1, 0x76, 0x05, 0x52), DecoderProfiles.DXVA_ModeH264_VLD_Stereo_NoFGT },
    //    { new(0x705b9d82, 0x76cf,0x49d6, 0xb7, 0xe6, 0xac, 0x88, 0x72, 0xdb, 0x01, 0x3c), DecoderProfiles.DXVA_ModeH264_VLD_Multiview_NoFGT },
    //    { new(0x1b81be80, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeWMV8_A },
    //    { new(0x1b81be81, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeWMV8_B },
    //    { new(0x1b81be90, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeWMV9_A },
    //    { new(0x1b81be91, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeWMV9_B },
    //    { new(0x1b81be94, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeWMV9_C },
    //    { new(0x1b81beA0, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeVC1_A },
    //    { new(0x1b81beA1, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeVC1_B },
    //    { new(0x1b81beA2, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeVC1_C },
    //    { new(0x1b81beA3, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeVC1_D },
    //    { new(0x1b81bea4, 0xa0c7,0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeVC1_D2010 },
    //    { new(0xefd64d74, 0xc9e8,0x41d7, 0xa5, 0xe9, 0xe9, 0xb0, 0xe3, 0x9f, 0xa3, 0x19), DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_Simple },
    //    { new(0xed418a9f, 0x010d,0x4eda, 0x9a, 0xe3, 0x9a, 0x65, 0x35, 0x8d, 0x8d, 0x2e), DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_NoGMC },
    //    { new(0xab998b5b, 0x4258,0x44a9, 0x9f, 0xeb, 0x94, 0xe5, 0x97, 0xa6, 0xba, 0xae), DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_GMC },
    //    { new(0x5b11d51b, 0x2f4c,0x4452, 0xbc, 0xc3, 0x09, 0xf2, 0xa1, 0x16, 0x0c, 0xc0), DecoderProfiles.DXVA_ModeHEVC_VLD_Main },
    //    { new(0x107af0e0, 0xef1a,0x4d19, 0xab, 0xa8, 0x67, 0xa1, 0x63, 0x07, 0x3d, 0x13), DecoderProfiles.DXVA_ModeHEVC_VLD_Main10 },
    //    { new(0x90b899ea, 0x3a62,0x4705, 0x88, 0xb3, 0x8d, 0xf0, 0x4b, 0x27, 0x44, 0xe7), DecoderProfiles.DXVA_ModeVP8_VLD },
    //    { new(0x463707f8, 0xa1d0,0x4585, 0x87, 0x6d, 0x83, 0xaa, 0x6d, 0x60, 0xb8, 0x9e), DecoderProfiles.DXVA_ModeVP9_VLD_Profile0 },
    //    { new(0xa4c749ef, 0x6ecf,0x48aa, 0x84, 0x48, 0x50, 0xa7, 0xa1, 0x16, 0x5f, 0xf7), DecoderProfiles.DXVA_ModeVP9_VLD_10bit_Profile2 },
    //    { new(0x1b81be09, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeMPEG1_A },
    //    { new(0x1b81be0A, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeMPEG2_A },
    //    { new(0x1b81be0B, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeMPEG2_B },
    //    { new(0x1b81be0C, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeMPEG2_C },
    //    { new(0x1b81be0D, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeMPEG2_D },
    //    { new(0x1b81be01, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH261_A },
    //    { new(0x1b81be02, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH261_B },
    //    { new(0x1b81be03, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_A },
    //    { new(0x1b81be04, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_B },
    //    { new(0x1b81be05, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_C },
    //    { new(0x1b81be06, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_D },
    //    { new(0x1b81be07, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_E },
    //    { new(0x1b81be08, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5), DecoderProfiles.DXVA_ModeH263_F },
    //    { new(0xd5f04ff9, 0x3418, 0x45d8, 0x95, 0x61, 0x32, 0xa7, 0x6a, 0xae, 0x2d, 0xdd), DecoderProfiles.DXVA_ModeH264_VLD_WithFMOASO_NoFGT },
    //    { new(0x9901CCD3, 0xca12, 0x4b7e, 0x86, 0x7a, 0xe2, 0x22, 0x3d, 0x92, 0x55, 0xc3), DecoderProfiles.DXVA_ModeH264_VLD_Multiview },
    //    { new(0x604F8E64, 0x4951, 0x4c54, 0x88, 0xFE, 0xAB, 0xD2, 0x5C, 0x15, 0xB3, 0xD6), DecoderProfiles.DXVADDI_Intel_ModeH264_A },
    //    { new(0x604F8E66, 0x4951, 0x4c54, 0x88, 0xFE, 0xAB, 0xD2, 0x5C, 0x15, 0xB3, 0xD6), DecoderProfiles.DXVADDI_Intel_ModeH264_C },
    //    { new(0x604F8E68, 0x4951, 0x4c54, 0x88, 0xFE, 0xAB, 0xD2, 0x5C, 0x15, 0xB3, 0xD6), DecoderProfiles.DXVA_Intel_H264_NoFGT_ClearVideo },
    //    { new(0x4245F676, 0x2BBC, 0x4166, 0xa0, 0xBB, 0x54, 0xE7, 0xB8, 0x49, 0xC3, 0x80), DecoderProfiles.DXVA_ModeH264_VLD_NoFGT_Flash },
    //    { new(0xBCC5DB6D, 0xA2B6, 0x4AF0, 0xAC, 0xE4, 0xAD, 0xB1, 0xF7, 0x87, 0xBC, 0x89), DecoderProfiles.DXVA_Intel_VC1_ClearVideo },
    //    { new(0xE07EC519, 0xE651, 0x4CD6, 0xAC, 0x84, 0x13, 0x70, 0xCC, 0xEE, 0xC8, 0x51), DecoderProfiles.DXVA_Intel_VC1_ClearVideo_2 },
    //    { new(0x9947EC6F, 0x689B, 0x11DC, 0xA3, 0x20, 0x00, 0x19, 0xDB, 0xBC, 0x41, 0x84), DecoderProfiles.DXVA_nVidia_MPEG4_ASP },
    //    { new(0x7C74ADC6, 0xe2ba, 0x4ade, 0x86, 0xde, 0x30, 0xbe, 0xab, 0xb4, 0x0c, 0xc1), DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_Avivo },
    //    { new(0x8c56eb1e, 0x2b47, 0x466f, 0x8d, 0x33, 0x7d, 0xbc, 0xd6, 0x3f, 0x3d, 0xf2), DecoderProfiles.DXVA_ModeHEVC_VLD_Main_Intel },
    //    { new(0x75fc75f7, 0xc589, 0x4a07, 0xa2, 0x5b, 0x72, 0xe0, 0x3b, 0x03, 0x83, 0xb3), DecoderProfiles.DXVA_ModeHEVC_VLD_Main10_Intel },
    //    { new(0x8ff8a3aa, 0xc456, 0x4132, 0xb6, 0xef, 0x69, 0xd9, 0xdd, 0x72, 0x57, 0x1d), DecoderProfiles.DXVA_ModeHEVC_VLD_Main12_Intel },
    //    { new(0xe484dcb8, 0xcac9, 0x4859, 0x99, 0xf5, 0x5c, 0x0d, 0x45, 0x06, 0x90, 0x89), DecoderProfiles.DXVA_ModeHEVC_VLD_Main422_10_Intel },
    //    { new(0xc23dd857, 0x874b, 0x423c, 0xb6, 0xe0, 0x82, 0xce, 0xaa, 0x9b, 0x11, 0x8a), DecoderProfiles.DXVA_ModeHEVC_VLD_Main422_12_Intel },
    //    { new(0x41a5af96, 0xe415, 0x4b0c, 0x9d, 0x03, 0x90, 0x78, 0x58, 0xe2, 0x3e, 0x78), DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_Intel },
    //    { new(0x6a6a81ba, 0x912a, 0x485d, 0xb5, 0x7f, 0xcc, 0xd2, 0xd3, 0x7b, 0x8d, 0x94), DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_10_Intel },
    //    { new(0x5b08e35d, 0x0c66, 0x4c51, 0xa6, 0xf1, 0x89, 0xd0, 0x0c, 0xb2, 0xc1, 0x97), DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_12_Intel },
    //    { new(0xc30700c4, 0xe384, 0x43e0, 0xb9, 0x82, 0x2d, 0x89, 0xee, 0x7f, 0x77, 0xc4), DecoderProfiles.DXVA_ModeH264_VLD_SVC_Scalable_Baseline },
    //    { new(0x9b8175d4, 0xd670, 0x4cf2, 0xa9, 0xf0, 0xfa, 0x56, 0xdf, 0x71, 0xa1, 0xae), DecoderProfiles.DXVA_ModeH264_VLD_SVC_Restricted_Scalable_Baseline },
    //    { new(0x728012c9, 0x66a8, 0x422f, 0x97, 0xe9, 0xb5, 0xe3, 0x9b, 0x51, 0xc0, 0x53), DecoderProfiles.DXVA_ModeH264_VLD_SVC_Scalable_High },
    //    { new(0x8efa5926, 0xbd9e, 0x4b04, 0x8b, 0x72, 0x8f, 0x97, 0x7d, 0xc4, 0x4c, 0x36), DecoderProfiles.DXVA_ModeH264_VLD_SVC_Restricted_Scalable_High_Progressive },
    //    { new(0x76988a52, 0xdf13, 0x419a, 0x8e, 0x64, 0xff, 0xcf, 0x4a, 0x33, 0x6c, 0xf5), DecoderProfiles.DXVA_ModeVP9_VLD_Intel },
    //    { new(0xb8be4ccb, 0xcf53, 0x46ba, 0x8d, 0x59, 0xd6, 0xb8, 0xa6, 0xda, 0x5d, 0x2a), DecoderProfiles.DXVA_ModeAV1_VLD_Profile0 },
    //    { new(0x6936ff0f, 0x45b1, 0x4163, 0x9c, 0xc1, 0x64, 0x6e, 0xf6, 0x94, 0x61, 0x08), DecoderProfiles.DXVA_ModeAV1_VLD_Profile1 },
    //    { new(0x0c5f2aa1, 0xe541, 0x4089, 0xbb, 0x7b, 0x98, 0x11, 0x0a, 0x19, 0xd7, 0xc8), DecoderProfiles.DXVA_ModeAV1_VLD_Profile2 },
    //    { new(0x17127009, 0xa00f, 0x4ce1, 0x99, 0x4e, 0xbf, 0x40, 0x81, 0xf6, 0xf3, 0xf0), DecoderProfiles.DXVA_ModeAV1_VLD_12bit_Profile2 },
    //    { new(0x2d80bed6, 0x9cac, 0x4835, 0x9e, 0x91, 0x32, 0x7b, 0xbc, 0x4f, 0x9e, 0xe8), DecoderProfiles.DXVA_ModeAV1_VLD_12bit_Profile2_420 },

    //};
    //internal static Dictionary<DecoderProfiles, string> DXVADecoderProfilesDesc = new()
    //{
    //    { DecoderProfiles.DXVA_ModeMPEG1_A, "MPEG-1 decoder, restricted profile A" },
    //    { DecoderProfiles.DXVA_ModeMPEG2_A, "MPEG-2 decoder, restricted profile A" },
    //    { DecoderProfiles.DXVA_ModeMPEG2_B, "MPEG-2 decoder, restricted profile B" },
    //    { DecoderProfiles.DXVA_ModeMPEG2_C, "MPEG-2 decoder, restricted profile C" },
    //    { DecoderProfiles.DXVA_ModeMPEG2_D, "MPEG-2 decoder, restricted profile D" },
    //    { DecoderProfiles.DXVA2_ModeMPEG2_VLD, "MPEG-2 variable-length decoder" },
    //    { DecoderProfiles.DXVA_ModeMPEG2and1_VLD, "MPEG-2 & MPEG-1 variable-length decoder" },
    //    { DecoderProfiles.DXVA2_ModeMPEG2_MoComp, "MPEG-2 motion compensation" },
    //    { DecoderProfiles.DXVA2_ModeMPEG2_IDCT, "MPEG-2 inverse discrete cosine transform" },
    //    { DecoderProfiles.DXVA_ModeMPEG1_VLD, "MPEG-1 variable-length decoder, no D pictures" },
    //    { DecoderProfiles.DXVA_ModeH264_F, "H.264 variable-length decoder, film grain technology" },
    //    { DecoderProfiles.DXVA_ModeH264_E, "H.264 variable-length decoder, no film grain technology" },
    //    { DecoderProfiles.DXVA_Intel_H264_NoFGT_ClearVideo, "H.264 variable-length decoder, no film grain technology (Intel ClearVideo)" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_WithFMOASO_NoFGT, "H.264 variable-length decoder, no film grain technology, FMO/ASO" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_NoFGT_Flash, "H.264 variable-length decoder, no film grain technology, Flash" },
    //    { DecoderProfiles.DXVA_ModeH264_D, "H.264 inverse discrete cosine transform, film grain technology" },
    //    { DecoderProfiles.DXVA_ModeH264_C, "H.264 inverse discrete cosine transform, no film grain technology" },
    //    { DecoderProfiles.DXVADDI_Intel_ModeH264_C, "H.264 inverse discrete cosine transform, no film grain technology (Intel)" },
    //    { DecoderProfiles.DXVA_ModeH264_B, "H.264 motion compensation, film grain technology" },
    //    { DecoderProfiles.DXVA_ModeH264_A, "H.264 motion compensation, no film grain technology" },
    //    { DecoderProfiles.DXVADDI_Intel_ModeH264_A, "H.264 motion compensation, no film grain technology (Intel)" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_Stereo_Progressive_NoFGT, "H.264 stereo high profile, mbs flag set" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_Stereo_NoFGT, "H.264 stereo high profile" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_Multiview_NoFGT, "H.264 multiview high profile" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_SVC_Scalable_Baseline, "H.264 scalable video coding, Scalable Baseline Profile" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_SVC_Restricted_Scalable_Baseline, "H.264 scalable video coding, Scalable Constrained Baseline Profile" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_SVC_Scalable_High, "H.264 scalable video coding, Scalable High Profile" },
    //    { DecoderProfiles.DXVA_ModeH264_VLD_SVC_Restricted_Scalable_High_Progressive, "H.264 scalable video coding, Scalable Constrained High Profile" },
    //    { DecoderProfiles.DXVA_ModeWMV8_B, "Windows Media Video 8 motion compensation" },
    //    { DecoderProfiles.DXVA_ModeWMV8_A, "Windows Media Video 8 post processing" },
    //    { DecoderProfiles.DXVA_ModeWMV9_C, "Windows Media Video 9 IDCT" },
    //    { DecoderProfiles.DXVA_ModeWMV9_B, "Windows Media Video 9 motion compensation" },
    //    { DecoderProfiles.DXVA_ModeWMV9_A, "Windows Media Video 9 post processing" },
    //    { DecoderProfiles.DXVA_ModeVC1_D, "VC-1 variable-length decoder" },
    //    { DecoderProfiles.DXVA_ModeVC1_D2010, "VC-1 variable-length decoder" },
    //    { DecoderProfiles.DXVA_Intel_VC1_ClearVideo_2, "VC-1 variable-length decoder 2 (Intel)" },
    //    { DecoderProfiles.DXVA_Intel_VC1_ClearVideo, "VC-1 variable-length decoder (Intel)" },
    //    { DecoderProfiles.DXVA_ModeVC1_C, "VC-1 inverse discrete cosine transform" },
    //    { DecoderProfiles.DXVA_ModeVC1_B, "VC-1 motion compensation" },
    //    { DecoderProfiles.DXVA_ModeVC1_A, "VC-1 post processing" },
    //    { DecoderProfiles.DXVA_nVidia_MPEG4_ASP, "MPEG-4 Part 2 nVidia bitstream decoder" },
    //    { DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_Simple, "MPEG-4 Part 2 variable-length decoder, Simple Profile" },
    //    { DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_NoGMC, "MPEG-4 Part 2 variable-length decoder, Simple&Advanced Profile, no GMC" },
    //    { DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_GMC, "MPEG-4 Part 2 variable-length decoder, Simple&Advanced Profile, GMC" },
    //    { DecoderProfiles.DXVA_ModeMPEG4pt2_VLD_AdvSimple_Avivo, "MPEG-4 Part 2 variable-length decoder, Simple&Advanced Profile, Avivo" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main_Intel, "HEVC Main profile (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main10_Intel, "HEVC Main 10 profile (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main12_Intel, "HEVC Main profile 4:2:2 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main422_10_Intel, "HEVC Main 10 profile 4:2:2 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main422_12_Intel, "HEVC Main 12 profile 4:2:2 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_Intel, "HEVC Main profile 4:4:4 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_10_Intel, "HEVC Main 10 profile 4:4:4 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main444_12_Intel, "HEVC Main 12 profile 4:4:4 Range Extension (Intel)" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main, "HEVC Main profile" },
    //    { DecoderProfiles.DXVA_ModeHEVC_VLD_Main10, "HEVC Main 10 profile" },
    //    { DecoderProfiles.DXVA_ModeH261_A, "H.261 decoder, restricted profile A" },
    //    { DecoderProfiles.DXVA_ModeH261_B, "H.261 decoder, restricted profile B" },
    //    { DecoderProfiles.DXVA_ModeH263_A, "H.263 decoder, restricted profile A" },
    //    { DecoderProfiles.DXVA_ModeH263_B, "H.263 decoder, restricted profile B" },
    //    { DecoderProfiles.DXVA_ModeH263_C, "H.263 decoder, restricted profile C" },
    //    { DecoderProfiles.DXVA_ModeH263_D, "H.263 decoder, restricted profile D" },
    //    { DecoderProfiles.DXVA_ModeH263_E, "H.263 decoder, restricted profile E" },
    //    { DecoderProfiles.DXVA_ModeH263_F, "H.263 decoder, restricted profile F" },
    //    { DecoderProfiles.DXVA_ModeVP8_VLD, "VP8" },
    //    { DecoderProfiles.DXVA_ModeVP9_VLD_Profile0, "VP9 profile 0" },
    //    { DecoderProfiles.DXVA_ModeVP9_VLD_10bit_Profile2, "VP9 profile" },
    //    { DecoderProfiles.DXVA_ModeVP9_VLD_Intel, "VP9 profile Intel" },
    //    { DecoderProfiles.DXVA_ModeAV1_VLD_Profile0, "AV1 Main profile" },
    //    { DecoderProfiles.DXVA_ModeAV1_VLD_Profile1, "AV1 High profile" },
    //};
    #endregion
}

using System.Collections.Generic;
using System.Numerics;
using System.Text.Json.Serialization;
using Vortice.DXGI;
using Vortice.Direct3D11;

using ID3D11VideoContext = Vortice.Direct3D11.ID3D11VideoContext;

using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaPlayer;

using static FlyleafLib.Logger;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaFramework.MediaRenderer;

unsafe public partial class Renderer
{
    /* TODO
     * 1) Try to sync filters between Flyleaf and D3D11 video processors so we will not have to reset on change
     * 2) Filter default values will change when the device/adapter is changed
     */

    public static Dictionary<string, VideoProcessorCapsCache> VideoProcessorsCapsCache = new();

    internal static VideoProcessorFilter ConvertFromVideoProcessorFilterCaps(VideoProcessorFilterCaps filter)
    {
        switch (filter)
        {
            case VideoProcessorFilterCaps.Brightness:
                return VideoProcessorFilter.Brightness;
            case VideoProcessorFilterCaps.Contrast:
                return VideoProcessorFilter.Contrast;
            case VideoProcessorFilterCaps.Hue:
                return VideoProcessorFilter.Hue;
            case VideoProcessorFilterCaps.Saturation:
                return VideoProcessorFilter.Saturation;
            case VideoProcessorFilterCaps.EdgeEnhancement:
                return VideoProcessorFilter.EdgeEnhancement;
            case VideoProcessorFilterCaps.NoiseReduction:
                return VideoProcessorFilter.NoiseReduction;
            case VideoProcessorFilterCaps.AnamorphicScaling:
                return VideoProcessorFilter.AnamorphicScaling;
            case VideoProcessorFilterCaps.StereoAdjustment:
                return VideoProcessorFilter.StereoAdjustment;

            default:
                return VideoProcessorFilter.StereoAdjustment;
        }
    }
    internal static VideoProcessorFilterCaps ConvertFromVideoProcessorFilter(VideoProcessorFilter filter)
    {
        switch (filter)
        {
            case VideoProcessorFilter.Brightness:
                return VideoProcessorFilterCaps.Brightness;
            case VideoProcessorFilter.Contrast:
                return VideoProcessorFilterCaps.Contrast;
            case VideoProcessorFilter.Hue:
                return VideoProcessorFilterCaps.Hue;
            case VideoProcessorFilter.Saturation:
                return VideoProcessorFilterCaps.Saturation;
            case VideoProcessorFilter.EdgeEnhancement:
                return VideoProcessorFilterCaps.EdgeEnhancement;
            case VideoProcessorFilter.NoiseReduction:
                return VideoProcessorFilterCaps.NoiseReduction;
            case VideoProcessorFilter.AnamorphicScaling:
                return VideoProcessorFilterCaps.AnamorphicScaling;
            case VideoProcessorFilter.StereoAdjustment:
                return VideoProcessorFilterCaps.StereoAdjustment;

            default:
                return VideoProcessorFilterCaps.StereoAdjustment;
        }
    }
    internal static VideoFilter ConvertFromVideoProcessorFilterRange(VideoProcessorFilterRange filter) => new()
    {
        Minimum = filter.Minimum,
        Maximum = filter.Maximum,
        Value   = filter.Default,
        Step    = filter.Multiplier
    };

    VideoColor                          D3D11VPBackgroundColor;
    ID3D11VideoDevice1                  vd1;
    ID3D11VideoProcessor                vp;
    ID3D11VideoContext                  vc;
    ID3D11VideoProcessorEnumerator      vpe;
    ID3D11VideoProcessorInputView       vpiv;
    ID3D11VideoProcessorOutputView      vpov;

    VideoProcessorStream[]              vpsa    = new VideoProcessorStream[] { new VideoProcessorStream() { Enable = true } };
    VideoProcessorContentDescription    vpcd    = new()
        {
            Usage = VideoUsage.PlaybackNormal,
            InputFrameFormat = VideoFrameFormat.InterlacedTopFieldFirst,

            InputFrameRate  = new Rational(1, 1),
            OutputFrameRate = new Rational(1, 1),
        };
    VideoProcessorOutputViewDescription vpovd   = new() { ViewDimension = VideoProcessorOutputViewDimension.Texture2D };
    VideoProcessorInputViewDescription  vpivd   = new()
        {
            FourCC          = 0,
            ViewDimension   = VideoProcessorInputViewDimension.Texture2D,
            Texture2D       = new Texture2DVideoProcessorInputView() { MipSlice = 0, ArraySlice = 0 }
        };
    VideoProcessorColorSpace            inputColorSpace;
    VideoProcessorColorSpace            outputColorSpace;

    AVDynamicHDRPlus*                   hdrPlusData = null;
    AVContentLightMetadata              lightData   = new();
    AVMasteringDisplayMetadata          displayData = new();

    uint actualRotation;
    bool actualHFlip, actualVFlip;
    bool configLoadedChecked;

    void InitializeVideoProcessor()
    {
        lock (VideoProcessorsCapsCache)
            try
            {
                vpcd.InputWidth = 1;
                vpcd.InputHeight= 1;
                vpcd.OutputWidth = vpcd.InputWidth;
                vpcd.OutputHeight= vpcd.InputHeight;

                outputColorSpace = new VideoProcessorColorSpace()
                {
                    Usage           = 0,
                    RGB_Range       = 0,
                    YCbCr_Matrix    = 1,
                    YCbCr_xvYCC     = 0,
                    Nominal_Range   = 2
                };

                if (VideoProcessorsCapsCache.ContainsKey(Device.Tag.ToString()))
                {
                    if (VideoProcessorsCapsCache[Device.Tag.ToString()].Failed)
                    {
                        InitializeFilters();
                        return;
                    }

                    vd1 = Device.QueryInterface<ID3D11VideoDevice1>();
                    vc  = context.QueryInterface<ID3D11VideoContext1>();

                    vd1.CreateVideoProcessorEnumerator(ref vpcd, out vpe);

                    if (vpe == null)
                    {
                        VPFailed();
                        return;
                    }

                    // if (!VideoProcessorsCapsCache[Device.Tag.ToString()].TypeIndex != -1)
                    vd1.CreateVideoProcessor(vpe, (uint)VideoProcessorsCapsCache[Device.Tag.ToString()].TypeIndex, out vp);
                    InitializeFilters();

                    return;
                }

                VideoProcessorCapsCache cache = new();
                VideoProcessorsCapsCache.Add(Device.Tag.ToString(), cache);

                vd1 = Device.QueryInterface<ID3D11VideoDevice1>();
                vc  = context.QueryInterface<ID3D11VideoContext>();

                vd1.CreateVideoProcessorEnumerator(ref vpcd, out vpe);

                if (vpe == null || Device.FeatureLevel < Vortice.Direct3D.FeatureLevel.Level_10_0)
                {
                    VPFailed();
                    return;
                }

                var vpe1 = vpe.QueryInterface<ID3D11VideoProcessorEnumerator1>();
                bool supportHLG = vpe1.CheckVideoProcessorFormatConversion(Format.P010, ColorSpaceType.YcbcrStudioGhlgTopLeftP2020, Format.B8G8R8A8_UNorm, ColorSpaceType.RgbFullG22NoneP709);
                bool supportHDR10Limited = vpe1.CheckVideoProcessorFormatConversion(Format.P010, ColorSpaceType.YcbcrStudioG2084TopLeftP2020, Format.B8G8R8A8_UNorm, ColorSpaceType.RgbStudioG2084NoneP2020);

                var vpCaps = vpe.VideoProcessorCaps;
                string dump = "";

                if (CanDebug)
                {
                    dump += $"=====================================================\r\n";
                    dump += $"MaxInputStreams           {vpCaps.MaxInputStreams}\r\n";
                    dump += $"MaxStreamStates           {vpCaps.MaxStreamStates}\r\n";
                    dump += $"HDR10 Limited             {(supportHDR10Limited ? "yes" : "no")}\r\n";
                    dump += $"HLG                       {(supportHLG ? "yes" : "no")}\r\n";

                    dump += $"\n[Video Processor Device Caps]\r\n";
                    foreach (VideoProcessorDeviceCaps cap in Enum.GetValues(typeof(VideoProcessorDeviceCaps)))
                        dump += $"{cap,-25} {((vpCaps.DeviceCaps & cap) != 0 ? "yes" : "no")}\r\n";

                    dump += $"\n[Video Processor Feature Caps]\r\n";
                    foreach (VideoProcessorFeatureCaps cap in Enum.GetValues(typeof(VideoProcessorFeatureCaps)))
                        dump += $"{cap,-25} {((vpCaps.FeatureCaps & cap) != 0 ? "yes" : "no")}\r\n";

                    dump += $"\n[Video Processor Stereo Caps]\r\n";
                    foreach (VideoProcessorStereoCaps cap in Enum.GetValues(typeof(VideoProcessorStereoCaps)))
                        dump += $"{cap,-25} {((vpCaps.StereoCaps & cap) != 0 ? "yes" : "no")}\r\n";

                    dump += $"\n[Video Processor Input Format Caps]\r\n";
                    foreach (VideoProcessorFormatCaps cap in Enum.GetValues(typeof(VideoProcessorFormatCaps)))
                        dump += $"{cap,-25} {((vpCaps.InputFormatCaps & cap) != 0 ? "yes" : "no")}\r\n";

                    dump += $"\n[Video Processor Filter Caps]\r\n";
                }

                foreach (VideoProcessorFilterCaps filter in Enum.GetValues(typeof(VideoProcessorFilterCaps)))
                    if ((vpCaps.FilterCaps & filter) != 0)
                    {
                        vpe1.GetVideoProcessorFilterRange(ConvertFromVideoProcessorFilterCaps(filter), out var range);
                        if (CanDebug) dump += $"{filter,-25} [{range.Minimum,6} - {range.Maximum,4}] | x{range.Multiplier,4} | *{range.Default}\r\n";
                        var vf = ConvertFromVideoProcessorFilterRange(range);
                        vf.Filter = (VideoFilters)filter;
                        cache.Filters.Add((VideoFilters)filter, vf);
                    }
                    else if (CanDebug)
                        dump += $"{filter,-25} no\r\n";

                if (CanDebug)
                {
                    dump += $"\n[Video Processor Input Format Caps]\r\n";
                    foreach (VideoProcessorAutoStreamCaps cap in Enum.GetValues(typeof(VideoProcessorAutoStreamCaps)))
                        dump += $"{cap,-25} {((vpCaps.AutoStreamCaps & cap) != 0 ? "yes" : "no")}\r\n";
                }

                uint typeIndex = 0;
                VideoProcessorRateConversionCaps rcCap = new();
                for (uint i = 0; i < vpCaps.RateConversionCapsCount; i++)
                {
                    vpe.GetVideoProcessorRateConversionCaps(i, out rcCap);
                    VideoProcessorProcessorCaps pCaps = (VideoProcessorProcessorCaps) rcCap.ProcessorCaps;

                    if (CanDebug)
                    {
                        dump += $"\n[Video Processor Rate Conversion Caps #{i}]\r\n";

                        dump += $"\n\t[Video Processor Rate Conversion Caps]\r\n";
                        var fields = typeof(VideoProcessorRateConversionCaps).GetFields();
                        foreach (var field in fields)
                            dump += $"\t{field.Name,-35} {field.GetValue(rcCap)}\r\n";

                        dump += $"\n\t[Video Processor Processor Caps]\r\n";
                        foreach (VideoProcessorProcessorCaps cap in Enum.GetValues(typeof(VideoProcessorProcessorCaps)))
                            dump += $"\t{cap,-35} {(((VideoProcessorProcessorCaps)rcCap.ProcessorCaps & cap) != 0 ? "yes" : "no")}\r\n";
                    }

                    typeIndex = i;

                    if (((VideoProcessorProcessorCaps)rcCap.ProcessorCaps & VideoProcessorProcessorCaps.DeinterlaceBob) != 0)
                        break; // TBR: When we add past/future frames support
                }
                vpe1.Dispose();

                if (CanDebug) Log.Debug($"D3D11 Video Processor\r\n{dump}");

                cache.TypeIndex = (int)typeIndex;
                cache.HLG = supportHLG;
                cache.HDR10Limited = supportHDR10Limited;
                cache.VideoProcessorCaps = vpCaps;
                cache.VideoProcessorRateConversionCaps = rcCap;

                //if (typeIndex != -1)
                vd1.CreateVideoProcessor(vpe, (uint)typeIndex, out vp);
                if (vp == null)
                {
                    VPFailed();
                    return;
                }

                cache.Failed = false;
                Log.Info($"D3D11 Video Processor Initialized (Rate Caps #{typeIndex})");

            } catch { DisposeVideoProcessor(); Log.Error($"D3D11 Video Processor Initialization Failed"); }

        InitializeFilters();
    }
    void VPFailed()
    {
        Log.Error($"D3D11 Video Processor Initialization Failed");

        if (!VideoProcessorsCapsCache.ContainsKey(Device.Tag.ToString()))
            VideoProcessorsCapsCache.Add(Device.Tag.ToString(), new VideoProcessorCapsCache());
        VideoProcessorsCapsCache[Device.Tag.ToString()].Failed = true;

        VideoProcessorsCapsCache[Device.Tag.ToString()].Filters.Add(VideoFilters.Brightness, new VideoFilter()  {  Filter = VideoFilters.Brightness });
        VideoProcessorsCapsCache[Device.Tag.ToString()].Filters.Add(VideoFilters.Contrast, new VideoFilter()    {  Filter = VideoFilters.Contrast });

        DisposeVideoProcessor();
        InitializeFilters();
    }
    void DisposeVideoProcessor()
    {
        vpiv?.Dispose();
        vpov?.Dispose();
        vp?.  Dispose();
        vpe?. Dispose();
        vc?.  Dispose();
        vd1?. Dispose();

        vc = null;
    }
    void InitializeFilters()
    {
        Filters = VideoProcessorsCapsCache[Device.Tag.ToString()].Filters;

        // Add FLVP filters if D3D11VP does not support them
        if (!Filters.ContainsKey(VideoFilters.Brightness))
            Filters.Add(VideoFilters.Brightness, new VideoFilter(VideoFilters.Brightness));

        if (!Filters.ContainsKey(VideoFilters.Contrast))
            Filters.Add(VideoFilters.Contrast, new VideoFilter(VideoFilters.Contrast));

        foreach(var filter in Filters.Values)
        {
            if (!Config.Video.Filters.ContainsKey(filter.Filter))
                continue;

            var cfgFilter = Config.Video.Filters[filter.Filter];
            cfgFilter.Available = true;
            cfgFilter.renderer = this;

            if (!configLoadedChecked && !Config.Loaded)
            {
                cfgFilter.Minimum       = filter.Minimum;
                cfgFilter.Maximum       = filter.Maximum;
                cfgFilter.DefaultValue  = filter.Value;
                cfgFilter.Value         = filter.Value;
                cfgFilter.Step          = filter.Step;
            }

            UpdateFilterValue(cfgFilter);
        }

        configLoadedChecked = true;
        UpdateBackgroundColor();

        if (vc != null)
        {
            vc.VideoProcessorSetStreamAutoProcessingMode(vp, 0, false);
            vc.VideoProcessorSetStreamFrameFormat(vp, 0, !Config.Video.Deinterlace ? VideoFrameFormat.Progressive : (Config.Video.DeinterlaceBottomFirst ? VideoFrameFormat.InterlacedBottomFieldFirst : VideoFrameFormat.InterlacedTopFieldFirst));
        }

        // Reset FLVP filters to defaults (can be different from D3D11VP filters scaling)
        if (videoProcessor == VideoProcessors.Flyleaf)
        {
            Config.Video.Filters[VideoFilters.Brightness].Value = Config.Video.Filters[VideoFilters.Brightness].Minimum + ((Config.Video.Filters[VideoFilters.Brightness].Maximum - Config.Video.Filters[VideoFilters.Brightness].Minimum) / 2);
            Config.Video.Filters[VideoFilters.Contrast].Value   = Config.Video.Filters[VideoFilters.Contrast].Minimum + ((Config.Video.Filters[VideoFilters.Contrast].Maximum - Config.Video.Filters[VideoFilters.Contrast].Minimum) / 2);
        }
    }

    internal void UpdateBackgroundColor()
    {
        D3D11VPBackgroundColor.Rgba.R = Scale(Config.Video.BackgroundColor.R, 0, 255, 0, 100) / 100.0f;
        D3D11VPBackgroundColor.Rgba.G = Scale(Config.Video.BackgroundColor.G, 0, 255, 0, 100) / 100.0f;
        D3D11VPBackgroundColor.Rgba.B = Scale(Config.Video.BackgroundColor.B, 0, 255, 0, 100) / 100.0f;

        vc?.VideoProcessorSetOutputBackgroundColor(vp, false, D3D11VPBackgroundColor);

        Present();
    }
    internal void UpdateDeinterlace()
    {
        lock (lockDevice)
        {
            if (Disposed)
                return;

            vc?.VideoProcessorSetStreamFrameFormat(vp, 0, !Config.Video.Deinterlace ? VideoFrameFormat.Progressive : (Config.Video.DeinterlaceBottomFirst ? VideoFrameFormat.InterlacedBottomFieldFirst : VideoFrameFormat.InterlacedTopFieldFirst));

            if (Config.Video.VideoProcessor != VideoProcessors.Auto)
                return;

            if (parent != null)
                return;

            ConfigPlanes();
            Present();
        }
    }
    internal void UpdateFilterValue(VideoFilter filter)
    {
        // D3D11VP
        if (Filters.ContainsKey(filter.Filter) && vc != null)
        {
            int scaledValue = (int) Scale(filter.Value, filter.Minimum, filter.Maximum, Filters[filter.Filter].Minimum, Filters[filter.Filter].Maximum);
            vc.VideoProcessorSetStreamFilter(vp, 0, ConvertFromVideoProcessorFilterCaps((VideoProcessorFilterCaps)filter.Filter), true, scaledValue);
        }

        if (parent != null)
            return;

        // FLVP
        switch (filter.Filter)
        {
            case VideoFilters.Brightness:
                int scaledValue = (int) Scale(filter.Value, filter.Minimum, filter.Maximum, 0, 100);
                psBufferData.brightness = scaledValue / 100.0f;
                context.UpdateSubresource(psBufferData, psBuffer);

                break;

            case VideoFilters.Contrast:
                scaledValue = (int) Scale(filter.Value, filter.Minimum, filter.Maximum, 0, 100);
                psBufferData.contrast = scaledValue / 100.0f;
                context.UpdateSubresource(psBufferData, psBuffer);

                break;

            default:
                break;
        }

        Present();
    }
    internal void UpdateHDRtoSDR(bool updateResource = true)
    {
        if(parent != null)
            return;

        float lum1 = 400;

        if (hdrPlusData != null)
        {
            lum1 = (float) (av_q2d(hdrPlusData->@params[0].average_maxrgb) * 100000.0);

            // this is not accurate more research required
            if (lum1 < 100)
                lum1 *= 10;
            lum1 = Math.Max(lum1, 400);
        }
        else if (Config.Video.HDRtoSDRMethod != HDRtoSDRMethod.Reinhard)
        {
            float lum2 = lum1;
            float lum3 = lum1;

            double lum = displayData.has_luminance != 0 ? av_q2d(displayData.max_luminance) : 400;

            if (lightData.MaxCLL > 0)
            {
                if (lightData.MaxCLL >= lum)
                {
                    lum1 = (float)lum;
                    lum2 = lightData.MaxCLL;
                }
                else
                {
                    lum1 = lightData.MaxCLL;
                    lum2 = (float)lum;
                }
                lum3 = lightData.MaxFALL;
                lum1 = (lum1 * 0.5f) + (lum2 * 0.2f) + (lum3 * 0.3f);
            }
            else
            {
                lum1 = (float)lum;
            }
        }
        else
        {
            if (lightData.MaxCLL > 0)
                lum1 = lightData.MaxCLL;
            else if (displayData.has_luminance != 0)
                lum1 = (float)av_q2d(displayData.max_luminance);
        }

        psBufferData.hdrmethod = Config.Video.HDRtoSDRMethod;

        if (psBufferData.hdrmethod == HDRtoSDRMethod.Hable)
        {
            psBufferData.g_luminance = lum1 > 1 ? lum1 : 400.0f;
            psBufferData.g_toneP1 = 10000.0f / psBufferData.g_luminance * (2.0f / Config.Video.HDRtoSDRTone);
            psBufferData.g_toneP2 = psBufferData.g_luminance / (100.0f * Config.Video.HDRtoSDRTone);
        }
        else if (psBufferData.hdrmethod == HDRtoSDRMethod.Reinhard)
        {
            psBufferData.g_toneP1 = lum1 > 0 ? (float)(Math.Log10(100) / Math.Log10(lum1)) : 0.72f;
            if (psBufferData.g_toneP1 < 0.1f || psBufferData.g_toneP1 > 5.0f)
                psBufferData.g_toneP1 = 0.72f;

            psBufferData.g_toneP1 *= Config.Video.HDRtoSDRTone;
        }
        else if (psBufferData.hdrmethod == HDRtoSDRMethod.Aces)
        {
            psBufferData.g_luminance = lum1 > 1 ? lum1 : 400.0f;
            psBufferData.g_toneP1 = Config.Video.HDRtoSDRTone;
        }

        if (updateResource)
        {
            context.UpdateSubresource(psBufferData, psBuffer);
            if (!VideoDecoder.IsRunning)
                Present();
        }
    }
    void UpdateRotation(uint angle, bool refresh = true)
    {
        _RotationAngle = angle;

        uint newRotation = _RotationAngle;

        if (VideoStream != null)
            newRotation += (uint)VideoStream.Rotation;

        if (rotationLinesize)
            newRotation += 180;

        newRotation %= 360;

        if (Disposed || (actualRotation == newRotation && actualHFlip == _HFlip && actualVFlip == _VFlip))
            return;

        bool hvFlipChanged = (actualHFlip || actualVFlip) != (_HFlip || _VFlip);

        actualRotation  = newRotation;
        actualHFlip     = _HFlip;
        actualVFlip     = _VFlip;

        if (actualRotation < 45 || actualRotation == 360)
            _d3d11vpRotation = VideoProcessorRotation.Identity;
        else if (actualRotation < 135)
            _d3d11vpRotation = VideoProcessorRotation.Rotation90;
        else if (actualRotation < 225)
            _d3d11vpRotation = VideoProcessorRotation.Rotation180;
        else if (actualRotation < 360)
            _d3d11vpRotation = VideoProcessorRotation.Rotation270;

        vsBufferData.mat  = Matrix4x4.CreateFromYawPitchRoll(0.0f, 0.0f, (float) (Math.PI / 180 * actualRotation));

        if (_HFlip || _VFlip)
        {
            vsBufferData.mat *= Matrix4x4.CreateScale(_HFlip ? -1 : 1, _VFlip ? -1 : 1, 1);
            if (hvFlipChanged)
            {
                // Renders both sides required for H-V Flip - TBR: consider for performance changing the vertex buffer / input layout instead?
                rasterizerState?.Dispose();
                rasterizerState = Device.CreateRasterizerState(new(CullMode.None, FillMode.Solid));
                context.RSSetState(rasterizerState);
            }
        }
        else if (hvFlipChanged)
        {
            // Removes back rendering for better performance
            rasterizerState?.Dispose();
            rasterizerState = Device.CreateRasterizerState(new(CullMode.Back, FillMode.Solid));
            context.RSSetState(rasterizerState);
        }

        if (parent == null)
            context.UpdateSubresource(vsBufferData, vsBuffer);

        if (child != null)
        {
            child.actualRotation    = actualRotation;
            child._d3d11vpRotation  = _d3d11vpRotation;
            child._RotationAngle    = _RotationAngle;
            child.rotationLinesize  = rotationLinesize;
            child.SetViewport();
        }

        vc?.VideoProcessorSetStreamRotation(vp, 0, true, _d3d11vpRotation);

        if (refresh)
            SetViewport();
    }
    internal void UpdateVideoProcessor()
    {
        if(parent != null)
            return;

        if (Config.Video.VideoProcessor == videoProcessor || (Config.Video.VideoProcessor == VideoProcessors.D3D11 && D3D11VPFailed))
            return;

        ConfigPlanes();
        Present();
    }
}

public class VideoFilter : NotifyPropertyChanged
{
    internal Renderer renderer;

    [JsonIgnore]
    public bool         Available   { get => _Available;    set => SetUI(ref _Available, value); }
    bool _Available;

    public VideoFilters Filter      { get => _Filter;       set => SetUI(ref _Filter, value); }
    VideoFilters _Filter = VideoFilters.Brightness;

    public int          Minimum     { get => _Minimum;      set => SetUI(ref _Minimum, value); }
    int _Minimum = 0;

    public int          Maximum     { get => _Maximum;      set => SetUI(ref _Maximum, value); }
    int _Maximum = 100;

    public float        Step        { get => _Step;         set => SetUI(ref _Step, value); }
    float _Step = 1;

    public int          DefaultValue
    {
        get;
        set
        {
            if (SetUI(ref field, value))
            {
                SetDefaultValue.OnCanExecuteChanged();
            }
        }
    } = 50;

    public int          Value
    {
        get;
        set
        {
            int v = value;
            v = Math.Min(v, Maximum);
            v = Math.Max(v, Minimum);

            if (Set(ref field, v))
            {
                renderer?.UpdateFilterValue(this);
                SetDefaultValue.OnCanExecuteChanged();
            }
        }
    } = 50;

    public RelayCommand SetDefaultValue => field ??= new(_ =>
    {
        Value = DefaultValue;
    }, _ => Value != DefaultValue);

    //internal void SetValue(int value) => SetUI(ref _Value, value, true, nameof(Value));

    public VideoFilter() { }
    public VideoFilter(VideoFilters filter, Player player = null)
        => Filter = filter;
}

public class VideoProcessorCapsCache
{
    public bool Failed = true;
    public int  TypeIndex = -1;
    public bool HLG;
    public bool HDR10Limited;
    public VideoProcessorCaps               VideoProcessorCaps;
    public VideoProcessorRateConversionCaps VideoProcessorRateConversionCaps;

    public Dictionary<VideoFilters, VideoFilter> Filters { get; set; } = new();
}

using System.Collections.ObjectModel;
using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaStream;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaPlayer;

public class SubsBitmap
{
    public WriteableBitmap Source { get; set; }
    public int X { get; set; }
    public int Y { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
}

public class SubsBitmapPosition : NotifyPropertyChanged
{
    private readonly Player _player;
    private readonly int _subIndex;

    public SubsBitmapPosition(Player player, int subIndex)
    {
        _player = player;
        _subIndex = subIndex;

        Calculate();
    }

    #region Config

    public double ConfScale
    {
        get;
        set
        {
            if (value <= 0)
            {
                return;
            }

            if (Set(ref field, Math.Round(value, 2)))
            {
                Calculate();
            }
        }
    } = 1.0; // x 1.0

    public double ConfPos
    {
        get;
        set
        {
            if (value < 0 || value > 150)
            {
                return;
            }

            if (Set(ref field, Math.Round(value)))
            {
                Calculate();
            }
        }
    } = 100; // 0 - 150

    #endregion

    #region WPF Property

    public Thickness? Margin { get; private set => Set(ref field, value); }

    public double ScaleX { get; private set => Set(ref field, value); } = 1.0;

    public double ScaleY { get; private set => Set(ref field, value); } = 1.0;

    /// <summary>
    /// True for horizontal subtitles
    /// Bitmap subtitles may be displayed vertically
    /// </summary>
    public bool IsHorizontal { get; private set; }

    #endregion

    public void Reset()
    {
        ConfScale = 1.0;
        ConfPos = 100;

        Margin = null;
        ScaleX = 1.0;
        ScaleY = 1.0;
        IsHorizontal = false;
    }

    public void Calculate()
    {
        if (_player.Subtitles == null ||
            _player.Subtitles[_subIndex].Data.Bitmap == null ||
            _player.SubtitlesDecoders[_subIndex].SubtitlesStream == null ||
            (_player.SubtitlesDecoders[_subIndex].Width == 0 && _player.SubtitlesManager[_subIndex].Width == 0) ||
            (_player.SubtitlesDecoders[_subIndex].Height == 0 && _player.SubtitlesManager[_subIndex].Height == 0))
        {
            return;
        }

        SubsBitmap bitmap = _player.Subtitles[_subIndex].Data.Bitmap;

        // Calculate the ratio of the current width of the window to the width of the video
        double renderWidth = _player.VideoDecoder.Renderer.GetViewport.Width;
        double videoWidth = _player.SubtitlesDecoders[_subIndex].Width;
        if (videoWidth == 0)
        {
            // Restore from cache because Width/Height may not be taken if the subtitle is not decoded enough.
            videoWidth = _player.SubtitlesManager[_subIndex].Width;
        }

        // double videoHeight_ = (int)(videoWidth / Player.VideoDemuxer.VideoStream.AspectRatio.Value);
        double renderHeight = _player.VideoDecoder.Renderer.GetViewport.Height;
        double videoHeight = _player.SubtitlesDecoders[_subIndex].Height;
        if (videoHeight == 0)
        {
            videoHeight = _player.SubtitlesManager[_subIndex].Height;
        }

        // In aspect ratio like a movie, a black background may be added to the top and bottom.
        // In this case, the subtitles should be placed based on the video display area, so the offset from the image rendering area excluding the black background should be taken into consideration.
        double yOffset = _player.renderer.GetViewport.Y;
        double xOffset = _player.renderer.GetViewport.X;

        double scaleFactorX = renderWidth / videoWidth;
        // double scaleFactorY = renderHeight / videoHeight;

        // Adjust subtitle size by the calculated ratio
        double scaleX = scaleFactorX;
        // double scaleY = scaleFactorY;

        // Note that if you are cropping videos with mkv in Handbrake, the subtitles will be crushed vertically if you don't use this one. vlc will crush them, but mpv will not have a problem.
        // It may be nice to be able to choose between the two.
        double scaleY = scaleX;

        double x = bitmap.X * scaleFactorX + xOffset;

        double yPosition = bitmap.Y / videoHeight;
        double y = renderHeight * yPosition + yOffset;

        // Adjust subtitle position(y - axis) by config.
        // However, if it detects that the subtitle is not a normal subtitle such as vertical subtitle, it will not be adjusted.
        // (Adjust only when it is below the center of the screen)
        // mpv: https://github.com/mpv-player/mpv/blob/df166c1333694cbfe70980dbded1984d48b0685a/sub/sd_lavc.c#L491-L494
        IsHorizontal = (bitmap.Y >= videoHeight / 2);
        if (IsHorizontal)
        {
            // mpv: https://github.com/mpv-player/mpv/blob/df166c1333694cbfe70980dbded1984d48b0685a/sub/sd_lavc.c#L486
            double offset = (100.0 - ConfPos) / 100.0 * videoHeight;
            y -= offset * scaleY;
        }

        // Adjust x and y axes when changing subtitle size(centering)
        if (ConfScale != 1.0)
        {
            // mpv: https://github.com/mpv-player/mpv/blob/df166c1333694cbfe70980dbded1984d48b0685a/sub/sd_lavc.c#L508-L515
            double ratio = (ConfScale - 1.0) / 2;

            double dw = bitmap.Width * scaleX;
            double dy = bitmap.Height * scaleY;

            x -= dw * ratio;
            y -= dy * ratio;

            scaleX *= ConfScale;
            scaleY *= ConfScale;
        }

        Margin = new Thickness(x, y, 0, 0);
        ScaleX = scaleX;
        ScaleY = scaleY;
    }
}

public class SubsData : NotifyPropertyChanged
{
#nullable enable
    private readonly Player _player;
    private readonly int _subIndex;
    private Subtitles Subs => _player.Subtitles;
    private Config Config => _player.Config;

    public SubsData(Player player, int subIndex)
    {
        _player = player;
        _subIndex = subIndex;

        BitmapPosition = new SubsBitmapPosition(_player, _subIndex);

        Config.Subtitles[_subIndex].PropertyChanged += SubConfigOnPropertyChanged;
    }

    private void SubConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(Config.SubConfig.Visible))
        {
            Raise(nameof(IsVisible));
            Raise(nameof(IsAbsoluteVisible));
            Raise(nameof(IsDisplaying));
        }
    }

    /// <summary>
    /// Subtitles Position (Position & Scale)
    /// </summary>
    public SubsBitmapPosition BitmapPosition { get; }

    /// <summary>
    /// Subtitles Bitmap
    /// </summary>
    public SubsBitmap? Bitmap
    {
        get;
        internal set
        {
            if (!Set(ref field, value))
            {
                return;
            }

            BitmapPosition.Calculate();

            if (Bitmap != null)
            {
                // TODO: L: When vertical and horizontal subtitles are displayed at the same time, the subtitles are displayed incorrectly
                // Absolute display of Primary sub
                // 1. If Primary is true and Secondary is false
                // 2. When a vertical subtitle is detected

                // Absolute display of Secondary sub
                // 1. When a vertical subtitle is detected
                bool isAbsolute = Subs[0].Enabled && !Subs[1].Enabled;
                if (!BitmapPosition.IsHorizontal)
                {
                    isAbsolute = true;
                }

                IsAbsolute = isAbsolute;
            }

            Raise(nameof(IsDisplaying));
        }
    }

    /// <summary>
    /// Whether subtitles are currently displayed or not
    /// (unless bitmap subtitles are displayed absolutely)
    /// </summary>
    public bool IsDisplaying => IsVisible && (Bitmap != null && !IsAbsolute // Bitmap subtitles, not absolute display
                                              ||
                                              !string.IsNullOrEmpty(Text)); // When text subtitles are available

    /// <summary>
    /// When vertical subtitle is detected in the case of bitmap sub, it becomes True and is displayed as absolute.
    /// </summary>
    public bool IsAbsolute
    {
        get;
        private set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(IsAbsoluteVisible));
                Raise(nameof(IsDisplaying));
            }
        }
    } = false;

    /// <summary>
    /// Bind target Used for switching Visibility
    /// </summary>
    public bool IsAbsoluteVisible => IsAbsolute && IsVisible;

    /// <summary>
    /// Bind target Used for switching Visibility
    /// </summary>
    public bool IsVisible => Config.Subtitles[_subIndex].Visible;

    /// <summary>
    /// Subtitles Text (updates dynamically while playing based on the duration that it should be displayed)
    /// </summary>
    public string Text
    {
        get;
        internal set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(IsDisplaying));
            }
        }
    } = "";

    public bool IsTranslated { get; set => Set(ref field, value); }

    public Language? Language { get; set => Set(ref field, value); }

    public void Reset()
    {
        Clear();
        UI(() =>
        {
            // Clear does not reset because there is a config in SubsBitmapPosition
            BitmapPosition.Reset();
        });
    }

    public void Clear()
    {
        if (Text != "" || Bitmap != null)
        {
            UI(() =>
            {
                Text = "";
                Bitmap = null;
            });
        }
    }
#nullable restore
}

/// <summary>
/// Preserve the state of streamIndex, URL, etc. of the currently selected subtitle in a global variable.
/// TODO: L: Cannot use multiple Players in one app, required to change this?
/// </summary>
public static class SubtitlesSelectedHelper
{
#nullable enable
    public static ValueTuple<int?, ExternalSubtitlesStream?>? Primary { get; set; }
    public static ValueTuple<int?, ExternalSubtitlesStream?>? Secondary { get; set; }

    public static SelectSubMethod PrimaryMethod { get; set; } = SelectSubMethod.Original;
    public static SelectSubMethod SecondaryMethod { get; set; } = SelectSubMethod.Original;

    /// <summary>
    /// Holds the index to switch (0: Primary, 1: Secondary)
    /// </summary>
    public static int CurIndex { get; set; } = 0;

    public static void Reset(int subIndex)
    {
        Debug.Assert(subIndex is 0 or 1);

        if (subIndex == 1)
        {
            Secondary = null;
            SecondaryMethod = SelectSubMethod.Original;
        }
        else
        {
            Primary = null;
            PrimaryMethod = SelectSubMethod.Original;
        }
    }

    public static SelectSubMethod GetMethod(int subIndex)
    {
        Debug.Assert(subIndex is 0 or 1);

        return subIndex == 1 ? SecondaryMethod : PrimaryMethod;
    }

    public static void SetMethod(int subIndex, SelectSubMethod method)
    {
        Debug.Assert(subIndex is 0 or 1);

        if (subIndex == 1)
        {
            SecondaryMethod = method;
        }
        else
        {
            PrimaryMethod = method;
        }
    }

    public static ValueTuple<int?, ExternalSubtitlesStream?>? Get(int subIndex)
    {
        Debug.Assert(subIndex is 0 or 1);
        return subIndex == 1 ? Secondary : Primary;
    }

    public static void Set(int subIndex, ValueTuple<int?, ExternalSubtitlesStream?>? tuple)
    {
        Debug.Assert(subIndex is 0 or 1);
        if (subIndex == 1)
        {
            Secondary = tuple;
        }
        else
        {
            Primary = tuple;
        }
    }

    public static bool GetSubEnabled(this StreamBase stream, int subIndex)
    {
        Debug.Assert(subIndex is 0 or 1);
        var tuple = subIndex == 1 ? Secondary : Primary;

        if (!tuple.HasValue)
        {
            return false;
        }

        if (tuple.Value.Item1.HasValue)
        {
            // internal sub
            return tuple.Value.Item1 == stream.StreamIndex;
        }

        if (tuple.Value.Item2 is not null && stream.ExternalStream is not null)
        {
            // external sub
            return GetSubEnabled(stream.ExternalStream, subIndex);
        }

        return false;
    }

    public static bool GetSubEnabled(this ExternalStream stream, int subIndex)
    {
        ArgumentNullException.ThrowIfNull(stream);

        return GetSubEnabled(stream.Url, subIndex);
    }

    public static bool GetSubEnabled(this string url, int subIndex)
    {
        Debug.Assert(subIndex is 0 or 1);
        var tuple = subIndex == 1 ? Secondary : Primary;

        if (!tuple.HasValue || tuple.Value.Item2 is null)
        {
            return false;
        }

        return tuple.Value.Item2.Url == url;
    }
#nullable restore
}

public class Subtitle : NotifyPropertyChanged
{
    private readonly Player _player;
    private readonly int _subIndex;

    private Config Config => _player.Config;
    private DecoderContext Decoder => _player?.decoder;

    public Subtitle(int subIndex, Player player)
    {
        _player = player;
        _subIndex = subIndex;

        Data = new SubsData(_player, _subIndex);
    }

    #pragma warning disable CS9266
    public bool EnabledASR { get => _enabledASR; private set => Set(ref field, value); }
    private bool _enabledASR;

    /// <summary>
    /// Whether the input has subtitles and it is configured
    /// </summary>
    public bool IsOpened { get => _isOpened; private set => Set(ref field, value); }
    private bool _isOpened;

    public string Codec { get => _codec; private set => Set(ref field, value); }
    private string _codec;

    public bool IsBitmap { get => _isBitmap; private set => Set(ref field, value); }
    private bool _isBitmap;
    #pragma warning restore CS9266

    public bool Enabled => IsOpened || EnabledASR;

    public SubsData Data { get; }

    private void UpdateUI()
    {
        UI(() =>
        {
            IsOpened = IsOpened;
            Codec = Codec;
            IsBitmap = IsBitmap;
            EnabledASR = EnabledASR;
        });
    }

    internal void Reset()
    {
        _codec = null;
        _isOpened = false;
        _isBitmap = false;
        _player.sFramesPrev[_subIndex] = null;
        // TODO: L: Here it may be null when switching subs while displaying.
        _player.sFrames[_subIndex] = null;
        _enabledASR = false;

        _player.SubtitlesASR.Reset(_subIndex);
        _player.SubtitlesOCR.Reset(_subIndex);
        _player.SubtitlesManager[_subIndex].Reset();

        _player.SubtitleClear(_subIndex);
        //_player.renderer?.ClearOverlayTexture();

        SubtitlesSelectedHelper.Reset(_subIndex);

        Data.Reset();
        UpdateUI();
    }

    internal void Refresh()
    {
        var subStream = Decoder.SubtitlesStreams[_subIndex];

        if (subStream == null)
        {
            Reset();
            return;
        }

        _codec = subStream.Codec;
        _isOpened = !Decoder.SubtitlesDecoders[_subIndex].Disposed;
        _isBitmap = subStream is { IsBitmap: true };

        // Update the selection state of automatically opened subtitles
        // (also manually updated but no problem as it is no change, necessary for primary subtitles)
        if (subStream.ExternalStream is ExternalSubtitlesStream extStream)
        {
            // external sub
            SubtitlesSelectedHelper.Set(_subIndex, (null, extStream));
        }
        else if (subStream.StreamIndex != -1)
        {
            // internal sub
            SubtitlesSelectedHelper.Set(_subIndex, (subStream.StreamIndex, null));
        }

        Data.Reset();
        _player.sFramesPrev[_subIndex] = null;
        _player.sFrames[_subIndex] = null;
        _player.SubtitleClear(_subIndex);
        //player.renderer?.ClearOverlayTexture();

        _enabledASR = false;
        _player.SubtitlesASR.Reset(_subIndex);
        _player.SubtitlesOCR.Reset(_subIndex);
        _player.SubtitlesManager[_subIndex].Reset();

        if (_player.renderer != null)
        {
            // Adjust bitmap subtitle size when resizing screen
            _player.renderer.ViewportChanged -= RendererOnViewportChanged;
            _player.renderer.ViewportChanged += RendererOnViewportChanged;
        }

        UpdateUI();

        // Create cache of the all subtitles in a separate thread
        Task.Run(Load)
            .ContinueWith(t =>
            {
                // TODO: L: error handling - restore state gracefully?
                if (t.IsFaulted)
                {
                    var ex = t.Exception.Flatten().InnerException;

                    _player.RaiseUnknownErrorOccurred($"Cannot load all subtitles on worker thread: {ex?.Message}", UnknownErrorType.Subtitles, ex);
                }
            }, TaskContinuationOptions.OnlyOnFaulted);
    }

    internal void Load()
    {
        SubtitlesStream stream = Decoder.SubtitlesStreams[_subIndex];
        ExternalSubtitlesStream extStream = stream.ExternalStream as ExternalSubtitlesStream;

        if (extStream != null)
        {
            // external sub
            // always load all bitmaps on memory with external subtitles
            _player.SubtitlesManager.Open(_subIndex, stream.Demuxer.Url, stream.StreamIndex, stream.Demuxer.Type, true, extStream.Language);
        }
        else
        {
            // internal sub
            // load all bitmaps on memory when cache enabled or OCR used
            bool useBitmap = Config.Subtitles.EnabledCached || SubtitlesSelectedHelper.GetMethod(_subIndex) == SelectSubMethod.OCR;
            _player.SubtitlesManager.Open(_subIndex, stream.Demuxer.Url, stream.StreamIndex, stream.Demuxer.Type, useBitmap, stream.Language);
        }

        TimeSpan curTime = new(_player.CurTime);

        _player.SubtitlesManager[_subIndex].SetCurrentTime(curTime);

        // Do OCR
        // TODO: L: When OCR is performed while bitmap stream is loaded, the stream is reset and the bitmap is loaded again.
        // Is it better to reuse loaded bitmap?
        if (SubtitlesSelectedHelper.GetMethod(_subIndex) == SelectSubMethod.OCR)
        {
            using (_player.SubtitlesManager[_subIndex].StartLoading())
            {
                _player.SubtitlesOCR.Do(_subIndex, _player.SubtitlesManager[_subIndex].Subs.ToList(), curTime);
            }
        }
    }

    internal void Enable()
    {
        if (!_player.CanPlay)
            return;

        // TODO: L: For some reason, there is a problem with subtitles temporarily not being
        // displayed, waiting for about 10 seconds will fix it.
        Decoder.OpenSuggestedSubtitles(_subIndex);

        _player.ReSync(Decoder.SubtitlesStreams[_subIndex], (int)(_player.CurTime / 10000), true);

        Refresh();
        UpdateUI();
    }
    internal void Disable()
    {
        if (!Enabled)
            return;

        Decoder.CloseSubtitles(_subIndex);
        Reset();
        UpdateUI();

        SubtitlesSelectedHelper.Reset(_subIndex);
    }

    private void RendererOnViewportChanged(object sender, EventArgs e)
    {
        Data.BitmapPosition.Calculate();
    }

    public void EnableASR()
    {
        _enabledASR = true;

        var url = _player.AudioDecoder.Demuxer.Url;
        var type = _player.AudioDecoder.Demuxer.Type;
        var streamIndex = _player.AudioDecoder.AudioStream.StreamIndex;

        TimeSpan curTime = new(_player.CurTime);

        Task.Run(() =>
            {
                bool isDone;

                using (_player.SubtitlesManager[_subIndex].StartLoading())
                {
                    isDone = _player.SubtitlesASR.Execute(_subIndex, url, streamIndex, type, curTime);
                }

                if (!isDone)
                {
                    // re-enable spinner because it is running
                    _player.SubtitlesManager[_subIndex].StartLoading();
                }
            })
            .ContinueWith(t =>
            {
                if (t.IsFaulted)
                {
                    if (_player.SubtitlesManager[_subIndex].Subs.Count == 0)
                    {
                        // reset if not single subtitles generated
                        Disable();
                    }

                    var ex = t.Exception.Flatten().InnerException;

                    _player.RaiseUnknownErrorOccurred($"Cannot execute ASR: {ex?.Message}", UnknownErrorType.ASR, t.Exception);
                }
            }, TaskContinuationOptions.OnlyOnFaulted);

        UpdateUI();
    }
}

public class Subtitles
{
    /// <summary>
    /// Embedded Streams
    /// </summary>
    public ObservableCollection<SubtitlesStream>
                    Streams         => _player.decoder?.VideoDemuxer.SubtitlesStreamsAll;

    private readonly Subtitle[] _subs;

    // indexer
    public Subtitle this[int i] => _subs[i];

    public Player Player => _player;

    private readonly Player _player;

    private Config Config => _player.Config;

    private int subNum => Config.Subtitles.Max;

    public Subtitles(Player player)
    {
        _player = player;
        _subs = new Subtitle[subNum];

        for (int i = 0; i < subNum; i++)
        {
            _subs[i] = new Subtitle(i, _player);
        }
    }

    internal void Enable()
    {
        for (int i = 0; i < subNum; i++)
        {
            this[i].Enable();
        }
    }

    internal void Disable()
    {
        for (int i = 0; i < subNum; i++)
        {
            this[i].Disable();
        }
    }

    internal void Reset()
    {
        for (int i = 0; i < subNum; i++)
        {
            this[i].Reset();
        }
    }

    internal void Refresh()
    {
        for (int i = 0; i < subNum; i++)
        {
            this[i].Refresh();
        }
    }

    // TODO: L: refactor
    public void DelayRemovePrimary()   => Config.Subtitles[0].Delay -= Config.Player.SubtitlesDelayOffset;
    public void DelayAddPrimary()      => Config.Subtitles[0].Delay += Config.Player.SubtitlesDelayOffset;
    public void DelayRemove2Primary()  => Config.Subtitles[0].Delay -= Config.Player.SubtitlesDelayOffset2;
    public void DelayAdd2Primary()     => Config.Subtitles[0].Delay += Config.Player.SubtitlesDelayOffset2;

    public void DelayRemoveSecondary()   => Config.Subtitles[1].Delay -= Config.Player.SubtitlesDelayOffset;
    public void DelayAddSecondary()      => Config.Subtitles[1].Delay += Config.Player.SubtitlesDelayOffset;
    public void DelayRemove2Secondary()  => Config.Subtitles[1].Delay -= Config.Player.SubtitlesDelayOffset2;
    public void DelayAdd2Secondary()     => Config.Subtitles[1].Delay += Config.Player.SubtitlesDelayOffset2;

    public void ToggleEnabled()        => Config.Subtitles.Enabled = !Config.Subtitles.Enabled;

    public void ToggleVisibility()
    {
        Config.Subtitles[0].Visible = !Config.Subtitles[0].Visible;
        Config.Subtitles[1].Visible = Config.Subtitles[0].Visible;
    }
    public void ToggleVisibilityPrimary()
    {
        Config.Subtitles[0].Visible = !Config.Subtitles[0].Visible;
    }

    public void ToggleVisibilitySecondary()
    {
        Config.Subtitles[1].Visible = !Config.Subtitles[1].Visible;
    }

    private bool _prevSeek(int subIndex)
    {
        var prev = _player.SubtitlesManager[subIndex].GetPrev();
        if (prev is not null)
        {
            _player.SeekAccurate(prev.StartTime, subIndex);
            return true;
        }

        return false;
    }

    public void PrevSeek() => _prevSeek(0);
    public void PrevSeek2() => _prevSeek(1);

    public void PrevSeekFallback()
    {
        if (!_prevSeek(0))
        {
            _player.SeekBackward2();
        }
    }
    public void PrevSeekFallback2()
    {
        if (!_prevSeek(1))
        {
            _player.SeekBackward2();
        }
    }

    private void _curSeek(int subIndex)
    {
        var cur = _player.SubtitlesManager[subIndex].GetCurrent();
        if (cur is not null)
        {
            _player.SeekAccurate(cur.StartTime, subIndex);
        }
        else
        {
            // fallback to prevSeek (same as mpv)
            _prevSeek(subIndex);
        }
    }

    public void CurSeek() => _curSeek(0);
    public void CurSeek2() => _curSeek(1);

    private bool _nextSeek(int subIndex)
    {
        var next = _player.SubtitlesManager[subIndex].GetNext();
        if (next is not null)
        {
            _player.SeekAccurate(next.StartTime, subIndex);
            return true;
        }
        return false;
    }

    public void NextSeek() => _nextSeek(0);
    public void NextSeek2() => _nextSeek(1);

    public void NextSeekFallback()
    {
        if (!_nextSeek(0))
        {
            _player.SeekForward2();
        }
    }
    public void NextSeekFallback2()
    {
        if (!_nextSeek(1))
        {
            _player.SeekForward2();
        }
    }
}

using System.Numerics;
using System.Text.RegularExpressions;

using Vortice;
using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DXGI;
using Vortice.DXGI.Debug;

using FlyleafLib.MediaFramework.MediaDecoder;

using static FlyleafLib.Logger;
using System.Runtime.InteropServices;

using ID3D11Device = Vortice.Direct3D11.ID3D11Device;
using ID3D11DeviceContext = Vortice.Direct3D11.ID3D11DeviceContext;

namespace FlyleafLib.MediaFramework.MediaRenderer;

public unsafe partial class Renderer
{
    static InputElementDescription[] inputElements =
    {
        new("POSITION", 0, Format.R32G32B32_Float,     0),
        new("TEXCOORD", 0, Format.R32G32_Float,        0),
    };

    static BufferDescription vertexBufferDesc = new()
    {
        BindFlags = BindFlags.VertexBuffer
    };

    static float[] vertexBufferData = new[]
    {
        -1.0f,  -1.0f,  0,      0.0f, 1.0f,
        -1.0f,   1.0f,  0,      0.0f, 0.0f,
         1.0f,  -1.0f,  0,      1.0f, 1.0f,

         1.0f,  -1.0f,  0,      1.0f, 1.0f,
        -1.0f,   1.0f,  0,      0.0f, 0.0f,
         1.0f,   1.0f,  0,      1.0f, 0.0f
    };

    static FeatureLevel[] featureLevelsAll = new[]
    {
        FeatureLevel.Level_12_1,
        FeatureLevel.Level_12_0,
        FeatureLevel.Level_11_1,
        FeatureLevel.Level_11_0,
        FeatureLevel.Level_10_1,
        FeatureLevel.Level_10_0,
        FeatureLevel.Level_9_3,
        FeatureLevel.Level_9_2,
        FeatureLevel.Level_9_1
    };

    static FeatureLevel[] featureLevels = new[]
    {
        FeatureLevel.Level_11_0,
        FeatureLevel.Level_10_1,
        FeatureLevel.Level_10_0,
        FeatureLevel.Level_9_3,
        FeatureLevel.Level_9_2,
        FeatureLevel.Level_9_1
    };

    static BlendDescription blendDesc = new();

    static Renderer()
    {
        blendDesc.RenderTarget[0].BlendEnable           = true;
        blendDesc.RenderTarget[0].SourceBlend           = Blend.SourceAlpha;
        blendDesc.RenderTarget[0].DestinationBlend      = Blend.InverseSourceAlpha;
        blendDesc.RenderTarget[0].BlendOperation        = BlendOperation.Add;
        blendDesc.RenderTarget[0].SourceBlendAlpha      = Blend.Zero;
        blendDesc.RenderTarget[0].DestinationBlendAlpha = Blend.Zero;
        blendDesc.RenderTarget[0].BlendOperationAlpha   = BlendOperation.Add;
        blendDesc.RenderTarget[0].RenderTargetWriteMask = ColorWriteEnable.All;
    }

    ID3D11DeviceContext context;

    ID3D11Buffer        vertexBuffer;
    ID3D11InputLayout   inputLayout;
    ID3D11RasterizerState
                        rasterizerState;
    ID3D11BlendState    blendStateAlpha;

    ID3D11VertexShader  ShaderVS;
    ID3D11PixelShader   ShaderPS;
    ID3D11PixelShader   ShaderBGRA;

    ID3D11Buffer        psBuffer;
    PSBufferType        psBufferData;

    ID3D11Buffer        vsBuffer;
    VSBufferType        vsBufferData;

    internal object     lockDevice = new();
    bool                isFlushing;

    public void Initialize(bool swapChain = true)
    {
        lock (lockDevice)
        {
            try
            {
                if (CanDebug) Log.Debug("Initializing");

                if (!Disposed)
                    Dispose();

                Disposed = false;

                ID3D11Device tempDevice;
                IDXGIAdapter1 adapter = null;
                var creationFlags       = DeviceCreationFlags.BgraSupport /*| DeviceCreationFlags.VideoSupport*/; // Let FFmpeg failed for VA if does not support it
                var creationFlagsWarp   = DeviceCreationFlags.None;

                #if DEBUG
                if (D3D11.SdkLayersAvailable())
                {
                    creationFlags       |= DeviceCreationFlags.Debug;
                    creationFlagsWarp   |= DeviceCreationFlags.Debug;
                }
                #endif

                // Finding User Definied adapter
                if (!string.IsNullOrWhiteSpace(Config.Video.GPUAdapter) && Config.Video.GPUAdapter.ToUpper() != "WARP")
                {
                    for (uint i=0; Engine.Video.Factory.EnumAdapters1(i, out adapter).Success; i++)
                    {
                        if (adapter.Description1.Description == Config.Video.GPUAdapter)
                            break;

                        if (Regex.IsMatch(adapter.Description1.Description + " luid=" + adapter.Description1.Luid, Config.Video.GPUAdapter, RegexOptions.IgnoreCase))
                            break;

                        adapter.Dispose();
                    }

                    if (adapter == null)
                    {
                        Log.Error($"GPU Adapter with {Config.Video.GPUAdapter} has not been found. Falling back to default.");
                        Config.Video.GPUAdapter = null;
                    }
                }

                // Creating WARP (force by user or us after late failure)
                if (!string.IsNullOrWhiteSpace(Config.Video.GPUAdapter) && Config.Video.GPUAdapter.ToUpper() == "WARP")
                    D3D11.D3D11CreateDevice(null, DriverType.Warp, creationFlagsWarp, featureLevels, out tempDevice).CheckError();

                // Creating User Defined or Default
                else
                {
                    // Creates the D3D11 Device based on selected adapter or default hardware (highest to lowest features and fall back to the WARP device. see http://go.microsoft.com/fwlink/?LinkId=286690)
                    if (D3D11.D3D11CreateDevice(adapter, adapter == null ? DriverType.Hardware : DriverType.Unknown, creationFlags, featureLevelsAll, out tempDevice).Failure)
                        if (D3D11.D3D11CreateDevice(adapter, adapter == null ? DriverType.Hardware : DriverType.Unknown, creationFlags, featureLevels, out tempDevice).Failure)
                        {
                            Config.Video.GPUAdapter = "WARP";
                            D3D11.D3D11CreateDevice(null, DriverType.Warp, creationFlagsWarp, featureLevels, out tempDevice).CheckError();
                        }
                }

                Device = tempDevice.QueryInterface<ID3D11Device1>();
                context= Device.ImmediateContext;

                // Gets the default adapter from the D3D11 Device
                if (adapter == null)
                {
                    Device.Tag = new Luid().ToString();
                    using var deviceTmp = Device.QueryInterface<IDXGIDevice1>();
                    using var adapterTmp = deviceTmp.GetAdapter();
                    adapter = adapterTmp.QueryInterface<IDXGIAdapter1>();
                }
                else
                    Device.Tag = adapter.Description.Luid.ToString();

                if (Engine.Video.GPUAdapters.ContainsKey(adapter.Description1.Luid))
                {
                    GPUAdapter = Engine.Video.GPUAdapters[adapter.Description1.Luid];
                    Config.Video.MaxVerticalResolutionAuto = GPUAdapter.MaxHeight;

                    if (CanDebug)
                    {
                        string dump = $"GPU Adapter\r\n{GPUAdapter}\r\n";

                        for (int i=0; i<GPUAdapter.Outputs.Count; i++)
                            dump += $"[Output #{i+1}] {GPUAdapter.Outputs[i]}\r\n";

                        Log.Debug(dump);
                    }
                }
                else
                    Log.Debug($"GPU Adapter: Unknown (Possible WARP without Luid)");

                tempDevice.Dispose();
                adapter.Dispose();

                using (var mthread    = Device.QueryInterface<ID3D11Multithread>()) mthread.SetMultithreadProtected(true);
                using (var dxgidevice = Device.QueryInterface<IDXGIDevice1>())      dxgidevice.MaximumFrameLatency = Config.Video.MaxFrameLatency;

                // Input Layout
                inputLayout = Device.CreateInputLayout(inputElements, ShaderCompiler.VSBlob);
                context.IASetInputLayout(inputLayout);
                context.IASetPrimitiveTopology(PrimitiveTopology.TriangleList);

                // Vertex Shader
                vertexBuffer = Device.CreateBuffer<float>(vertexBufferData, vertexBufferDesc);
                context.IASetVertexBuffer(0, vertexBuffer, sizeof(float) * 5);

                ShaderVS = Device.CreateVertexShader(ShaderCompiler.VSBlob);
                context.VSSetShader(ShaderVS);

                vsBuffer = Device.CreateBuffer(new()
                {
                    Usage           = ResourceUsage.Default,
                    BindFlags       = BindFlags.ConstantBuffer,
                    CPUAccessFlags  = CpuAccessFlags.None,
                    ByteWidth       = (uint)(sizeof(VSBufferType) + (16 - (sizeof(VSBufferType) % 16)))
                });
                context.VSSetConstantBuffer(0, vsBuffer);

                vsBufferData.mat = Matrix4x4.Identity;
                context.UpdateSubresource(vsBufferData, vsBuffer);

                // Pixel Shader
                InitPS();
                psBuffer = Device.CreateBuffer(new()
                {
                    Usage           = ResourceUsage.Default,
                    BindFlags       = BindFlags.ConstantBuffer,
                    CPUAccessFlags  = CpuAccessFlags.None,
                    ByteWidth       = (uint)(sizeof(PSBufferType) + (16 - (sizeof(PSBufferType) % 16)))
                });
                context.PSSetConstantBuffer(0, psBuffer);
                psBufferData.hdrmethod = HDRtoSDRMethod.None;
                context.UpdateSubresource(psBufferData, psBuffer);

                // subs
                ShaderBGRA = ShaderCompiler.CompilePS(Device, "bgra", @"color = float4(Texture1.Sample(Sampler, input.Texture).rgba);", null);

                // Blend State (currently used -mainly- for RGBA images and OverlayTexture)
                blendStateAlpha = Device.CreateBlendState(blendDesc);

                // Rasterizer (Will change CullMode to None for H-V Flip)
                rasterizerState = Device.CreateRasterizerState(new(CullMode.Back, FillMode.Solid));
                context.RSSetState(rasterizerState);

                InitializeVideoProcessor();

                if (CanInfo) Log.Info($"Initialized with Feature Level {(int)Device.FeatureLevel >> 12}.{((int)Device.FeatureLevel >> 8) & 0xf}");

            } catch (Exception e)
            {
                if (string.IsNullOrWhiteSpace(Config.Video.GPUAdapter) || Config.Video.GPUAdapter.ToUpper() != "WARP")
                {
                    try { if (Device != null) Log.Warn($"Device Remove Reason = {Device.DeviceRemovedReason.Description}"); } catch { } // For troubleshooting

                    Log.Warn($"Initialization failed ({e.Message}). Failling back to WARP device.");
                    Config.Video.GPUAdapter = "WARP";
                    Flush();
                }
                else
                {
                    Log.Error($"Initialization failed ({e.Message})");
                    Dispose();
                    return;
                }
            }

            if (swapChain)
            {
                if (ControlHandle != IntPtr.Zero)
                    InitializeSwapChain(ControlHandle);
                else if (SwapChainWinUIClbk != null)
                    InitializeWinUISwapChain();
            }

            InitializeChildSwapChain();
        }
    }
    public void InitializeChildSwapChain(bool swapChain = true)
    {
        if (child == null )
            return;

        lock (lockDevice)
        {
            child.lockDevice    = lockDevice;
            child.VideoDecoder  = VideoDecoder;
            child.Device        = Device;
            child.context       = context;
            child.curRatio      = curRatio;
            child.VideoRect     = VideoRect;
            child.videoProcessor= videoProcessor;
            child.InitializeVideoProcessor(); // to use the same VP we need to set it's config in each present (means we don't update VP config as is different)

            if (swapChain)
            {
                if (child.ControlHandle != IntPtr.Zero)
                    child.InitializeSwapChain(child.ControlHandle);
                else if (child.SwapChainWinUIClbk != null)
                    child.InitializeWinUISwapChain();
            }

            child.Disposed = false;
            child.SetViewport();
        }
    }

    public void Dispose()
    {
        lock (lockDevice)
        {
            if (Disposed)
                return;

            if (child != null)
                DisposeChild();

            Disposed = true;

            if (CanDebug) Log.Debug("Disposing");

            VideoDecoder.DisposeFrame(LastFrame);
            RefreshLayout();

            DisposeVideoProcessor();

            ShaderVS?.Dispose();
            ShaderPS?.Dispose();
            prevPSUniqueId = curPSUniqueId = ""; // Ensure we re-create ShaderPS for FlyleafVP on ConfigPlanes
            psBuffer?.Dispose();
            vsBuffer?.Dispose();
            inputLayout?.Dispose();
            vertexBuffer?.Dispose();
            rasterizerState?.Dispose();
            blendStateAlpha?.Dispose();
            DisposeSwapChain();

            overlayTexture?.Dispose();
            overlayTextureSrv?.Dispose();
            ShaderBGRA?.Dispose();

            singleGpu?.Dispose();
            singleStage?.Dispose();
            singleGpuRtv?.Dispose();
            singleStageDesc.Width = 0; // ensures re-allocation

            if (rtv2 != null)
            {
                for(int i = 0; i < rtv2.Length; i++)
                    rtv2[i].Dispose();

                rtv2 = null;
            }

            if (backBuffer2 != null)
            {
                for(int i = 0; i < backBuffer2.Length; i++)
                    backBuffer2[i]?.Dispose();

                backBuffer2 = null;
            }

            if (Device != null)
            {
                context.ClearState();
                context.Flush();
                context.Dispose();
                Device.Dispose();
                Device = null;
            }

            #if DEBUG
            ReportLiveObjects();
            #endif

            curRatio = 1.0f;
            if (CanInfo) Log.Info("Disposed");
        }
    }
    public void DisposeChild()
    {
        if (child == null)
            return;

        lock (lockDevice)
        {
            child.DisposeSwapChain();
            child.DisposeVideoProcessor();
            child.Disposed = true;

            if (!isFlushing)
            {
                child.Device        = null;
                child.context       = null;
                child.VideoDecoder  = null;
                child.LastFrame     = null;
                child               = null;
            }
        }
    }

    public void Flush()
    {
        lock (lockDevice)
        {
            isFlushing = true;
            var controlHandle = ControlHandle;
            var swapChainClbk = SwapChainWinUIClbk;

            IntPtr controlHandleReplica = IntPtr.Zero;
            Action<IDXGISwapChain2> swapChainClbkReplica = null;;
            if (child != null)
            {
                controlHandleReplica = child.ControlHandle;
                swapChainClbkReplica = child.SwapChainWinUIClbk;
            }

            Dispose();
            ControlHandle = controlHandle;
            SwapChainWinUIClbk = swapChainClbk;
            if (child != null)
            {
                child.ControlHandle = controlHandleReplica;
                child.SwapChainWinUIClbk = swapChainClbkReplica;
            }
            Initialize();
            isFlushing = false;
        }
    }

    #if DEBUG
    public static void ReportLiveObjects()
    {
        try
        {
            if (DXGI.DXGIGetDebugInterface1(out IDXGIDebug1 dxgiDebug).Success)
            {
                dxgiDebug.ReportLiveObjects(DXGI.DebugAll, ReportLiveObjectFlags.Summary | ReportLiveObjectFlags.IgnoreInternal);
                dxgiDebug.Dispose();
            }
        } catch { }
    }
    #endif

    [StructLayout(LayoutKind.Sequential)]
    struct PSBufferType
    {
        public int coefsIndex;
        public HDRtoSDRMethod hdrmethod;

        public float brightness;
        public float contrast;

        public float g_luminance;
        public float g_toneP1;
        public float g_toneP2;

        public float texWidth;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct VSBufferType
    {
        public Matrix4x4 mat;
    }
}

using System.Drawing.Imaging;
using System.Drawing;
using System.Threading;
using System.Windows.Media.Imaging;

using SharpGen.Runtime;

using Vortice;
using Vortice.Direct3D11;
using Vortice.DXGI;
using Vortice.Mathematics;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;

using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;

namespace FlyleafLib.MediaFramework.MediaRenderer;

public partial class Renderer
{
    // subs
    Texture2DDescription                    overlayTextureDesc;
    ID3D11Texture2D                         overlayTexture;
    ID3D11ShaderResourceView                overlayTextureSrv;
    int                                     overlayTextureOriginalWidth;
    int                                     overlayTextureOriginalHeight;
    int                                     overlayTextureOriginalPosX;
    int                                     overlayTextureOriginalPosY;

    ID3D11ShaderResourceView[]              overlayTextureSRVs = new ID3D11ShaderResourceView[1];

    // Used for off screen rendering
    Texture2DDescription                    singleStageDesc, singleGpuDesc;
    ID3D11Texture2D                         singleStage;
    ID3D11Texture2D                         singleGpu;
    ID3D11RenderTargetView                  singleGpuRtv;
    Viewport                                singleViewport;

    // Used for parallel off screen rendering
    ID3D11RenderTargetView[]                rtv2;
    ID3D11Texture2D[]                       backBuffer2;
    bool[]                                  backBuffer2busy;

    unsafe internal void PresentOffline(VideoFrame frame, ID3D11RenderTargetView rtv, Viewport viewport)
    {
        if (videoProcessor == VideoProcessors.D3D11)
        {
            var tmpResource = rtv.Resource;
            vd1.CreateVideoProcessorOutputView(tmpResource, vpe, vpovd, out var vpov);

            RawRect rect = new((int)viewport.X, (int)viewport.Y, (int)(viewport.Width + viewport.X), (int)(viewport.Height + viewport.Y));
            vc.VideoProcessorSetStreamSourceRect(vp, 0, true, VideoRect);
            vc.VideoProcessorSetStreamDestRect(vp, 0, true, rect);
            vc.VideoProcessorSetOutputTargetRect(vp, true, rect);

            if (frame.avFrame != null)
            {
                vpivd.Texture2D.ArraySlice = (uint) frame.avFrame->data[1];
                vd1.CreateVideoProcessorInputView(VideoDecoder.textureFFmpeg, vpe, vpivd, out vpiv);
            }
            else
            {
                vpivd.Texture2D.ArraySlice = 0;
                vd1.CreateVideoProcessorInputView(frame.textures[0], vpe, vpivd, out vpiv);
            }

            vpsa[0].InputSurface = vpiv;
            vc.VideoProcessorBlt(vp, vpov, 0, 1, vpsa);
            vpiv.Dispose();
            vpov.Dispose();
            tmpResource.Dispose();
        }
        else
        {
            context.OMSetRenderTargets(rtv);
            context.ClearRenderTargetView(rtv, Config.Video._BackgroundColor);
            context.RSSetViewport(viewport);
            context.PSSetShaderResources(0, frame.srvs);
            context.Draw(6, 0);
        }
    }

    /// <summary>
    /// Gets bitmap from a video frame
    /// </summary>
    /// <param name="width">Specify the width (-1: will keep the ratio based on height)</param>
    /// <param name="height">Specify the height (-1: will keep the ratio based on width)</param>
    /// <param name="frame">Video frame to process (null: will use the current/last frame)</param>
    /// <returns></returns>
    unsafe public Bitmap GetBitmap(int width = -1, int height = -1, VideoFrame frame = null)
    {
        try
        {
            lock (lockDevice)
            {
                frame ??= LastFrame;

                if (Disposed || frame == null || (frame.textures == null && frame.avFrame == null))
                    return null;

                if (width == -1 && height == -1)
                {
                    width  = VideoRect.Right;
                    height = VideoRect.Bottom;
                }
                else if (width != -1 && height == -1)
                    height = (int)(width / curRatio);
                else if (height != -1 && width == -1)
                    width  = (int)(height * curRatio);

                if (singleStageDesc.Width != width || singleStageDesc.Height != height)
                {
                    singleGpu?.Dispose();
                    singleStage?.Dispose();
                    singleGpuRtv?.Dispose();

                    singleStageDesc.Width   = (uint)width;
                    singleStageDesc.Height  = (uint)height;
                    singleGpuDesc.Width     = (uint)width;
                    singleGpuDesc.Height    = (uint)height;

                    singleStage = Device.CreateTexture2D(singleStageDesc);
                    singleGpu   = Device.CreateTexture2D(singleGpuDesc);
                    singleGpuRtv= Device.CreateRenderTargetView(singleGpu);

                    singleViewport = new Viewport(width, height);
                }

                PresentOffline(frame, singleGpuRtv, singleViewport);

                if (videoProcessor == VideoProcessors.D3D11)
                    SetViewport();
            }

            context.CopyResource(singleStage, singleGpu);
            return GetBitmap(singleStage);

        } catch (Exception e)
        {
            Log.Warn($"GetBitmap failed with: {e.Message}");
            return null;
        }
    }
    public Bitmap GetBitmap(ID3D11Texture2D stageTexture)
    {
        Bitmap bitmap   = new((int)stageTexture.Description.Width, (int)stageTexture.Description.Height);
        var db          = context.Map(stageTexture, 0, MapMode.Read, Vortice.Direct3D11.MapFlags.None);
        var bitmapData  = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

        if (db.RowPitch == bitmapData.Stride)
            MemoryHelpers.CopyMemory(bitmapData.Scan0, db.DataPointer, bitmap.Width * bitmap.Height * 4);
        else
        {
            var sourcePtr   = db.DataPointer;
            var destPtr     = bitmapData.Scan0;

            for (int y = 0; y < bitmap.Height; y++)
            {
                MemoryHelpers.CopyMemory(destPtr, sourcePtr, bitmap.Width * 4);

                sourcePtr   = IntPtr.Add(sourcePtr, (int)db.RowPitch);
                destPtr     = IntPtr.Add(destPtr, bitmapData.Stride);
            }
        }

        bitmap.UnlockBits(bitmapData);
        context.Unmap(stageTexture, 0);

        return bitmap;
    }
    /// <summary>
    /// Gets BitmapSource from a video frame
    /// </summary>
    /// <param name="width">Specify the width (-1: will keep the ratio based on height)</param>
    /// <param name="height">Specify the height (-1: will keep the ratio based on width)</param>
    /// <param name="frame">Video frame to process (null: will use the current/last frame)</param>
    /// <returns></returns>
    unsafe public BitmapSource GetBitmapSource(int width = -1, int height = -1, VideoFrame frame = null)
    {
        try
        {
            lock (lockDevice)
            {
                frame ??= LastFrame;

                if (Disposed || frame == null || (frame.textures == null && frame.avFrame == null))
                    return null;

                if (width == -1 && height == -1)
                {
                    width = VideoRect.Right;
                    height = VideoRect.Bottom;
                }
                else if (width != -1 && height == -1)
                    height = (int)(width / curRatio);
                else if (height != -1 && width == -1)
                    width = (int)(height * curRatio);

                if (singleStageDesc.Width != width || singleStageDesc.Height != height)
                {
                    singleGpu?.Dispose();
                    singleStage?.Dispose();
                    singleGpuRtv?.Dispose();

                    singleStageDesc.Width   = (uint)width;
                    singleStageDesc.Height  = (uint)height;
                    singleGpuDesc.Width     = (uint)width;
                    singleGpuDesc.Height    = (uint)height;

                    singleStage = Device.CreateTexture2D(singleStageDesc);
                    singleGpu = Device.CreateTexture2D(singleGpuDesc);
                    singleGpuRtv = Device.CreateRenderTargetView(singleGpu);

                    singleViewport = new Viewport(width, height);
                }

                PresentOffline(frame, singleGpuRtv, singleViewport);

                if (videoProcessor == VideoProcessors.D3D11)
                    SetViewport();
            }

            context.CopyResource(singleStage, singleGpu);
            return GetBitmapSource(singleStage);

        }
        catch (Exception e)
        {
            Log.Warn($"GetBitmapSource failed with: {e.Message}");
            return null;
        }
    }
    public BitmapSource GetBitmapSource(ID3D11Texture2D stageTexture)
    {
        WriteableBitmap bitmap = new((int)stageTexture.Description.Width, (int)stageTexture.Description.Height, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null);
        var db          = context.Map(stageTexture, 0, MapMode.Read, Vortice.Direct3D11.MapFlags.None);
        bitmap.Lock();

        if (db.RowPitch == bitmap.BackBufferStride)
            MemoryHelpers.CopyMemory(bitmap.BackBuffer, db.DataPointer, bitmap.PixelWidth * bitmap.PixelHeight * 4);
        else
        {
            var sourcePtr   = db.DataPointer;
            var destPtr     = bitmap.BackBuffer;

            for (int y = 0; y < bitmap.Height; y++)
            {
                MemoryHelpers.CopyMemory(destPtr, sourcePtr, bitmap.PixelWidth * 4);

                sourcePtr = IntPtr.Add(sourcePtr, (int)db.RowPitch);
                destPtr = IntPtr.Add(destPtr, bitmap.BackBufferStride);
            }
        }

        bitmap.Unlock();
        context.Unmap(stageTexture, 0);

        // Freezing animated wpf assets improves performance
        bitmap.Freeze();

        return bitmap;
    }

    /// <summary>
    /// Extracts a bitmap from a video frame
    /// (Currently cannot be used in parallel with the rendering)
    /// </summary>
    /// <param name="frame"></param>
    /// <returns></returns>
    public Bitmap ExtractFrame(VideoFrame frame)
    {
        if (Device == null || frame == null) return null;

        int subresource = -1;

        Texture2DDescription stageDesc = new()
        {
            Usage       = ResourceUsage.Staging,
            Width       = VideoDecoder.VideoStream.Width,
            Height      = VideoDecoder.VideoStream.Height,
            Format      = Format.B8G8R8A8_UNorm,
            ArraySize   = 1,
            MipLevels   = 1,
            BindFlags   = BindFlags.None,
            CPUAccessFlags      = CpuAccessFlags.Read,
            SampleDescription   = new SampleDescription(1, 0)
        };
        var stage = Device.CreateTexture2D(stageDesc);

        lock (lockDevice)
        {
            while (true)
            {
                for (int i=0; i<MaxOffScreenTextures; i++)
                    if (!backBuffer2busy[i]) { subresource = i; break;}

                if (subresource != -1)
                    break;
                else
                    Thread.Sleep(5);
            }

            backBuffer2busy[subresource] = true;
            PresentOffline(frame, rtv2[subresource], new Viewport(backBuffer2[subresource].Description.Width, backBuffer2[subresource].Description.Height));
            VideoDecoder.DisposeFrame(frame);

            context.CopyResource(stage, backBuffer2[subresource]);
            backBuffer2busy[subresource] = false;
        }

        var bitmap = GetBitmap(stage);
        stage.Dispose(); // TODO use array stage
        return bitmap;
    }

    private void PrepareForExtract()
    {
        if (rtv2 != null)
            for (int i = 0; i < rtv2.Length - 1; i++)
                rtv2[i].Dispose();

        if (backBuffer2 != null)
            for (int i = 0; i < backBuffer2.Length - 1; i++)
                backBuffer2[i].Dispose();

        backBuffer2busy = new bool[MaxOffScreenTextures];
        rtv2 = new ID3D11RenderTargetView[MaxOffScreenTextures];
        backBuffer2 = new ID3D11Texture2D[MaxOffScreenTextures];

        for (int i = 0; i < MaxOffScreenTextures; i++)
        {
            backBuffer2[i] = Device.CreateTexture2D(new Texture2DDescription()
            {
                Usage       = ResourceUsage.Default,
                BindFlags   = BindFlags.RenderTarget,
                Format      = Format.B8G8R8A8_UNorm,
                Width       = VideoStream.Width,
                Height      = VideoStream.Height,

                ArraySize   = 1,
                MipLevels   = 1,
                SampleDescription = new SampleDescription(1, 0)
            });

            rtv2[i] = Device.CreateRenderTargetView(backBuffer2[i]);
        }

        context.RSSetViewport(0, 0, VideoStream.Width, VideoStream.Height);
    }
}

using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using FlyleafLib;
using LLPlayer.Extensions;
using NMeCab.Specialized;

namespace LLPlayer.Controls;

public partial class SelectableSubtitleText : UserControl
{
    private readonly Binding _bindFontSize;
    private readonly Binding _bindFontWeight;
    private readonly Binding _bindFontFamily;
    private readonly Binding _bindFontStyle;
    private readonly Binding _bindFill;
    private readonly Binding _bindStroke;
    private readonly Binding _bindStrokeThicknessInitial;

    private OutlinedTextBlock? _wordStart;

    public SelectableSubtitleText()
    {
        InitializeComponent();

        // DataContext is set in WrapPanel, so there is no need to use ElementName.
        //_bindFontSize = new Binding(nameof(FontSize))
        //{
        //    //ElementName = nameof(Root),
        //    Mode = BindingMode.OneWay
        //};
        _bindFontSize = new Binding(nameof(FontSize));
        _bindFontWeight = new Binding(nameof(FontWeight));
        _bindFontFamily = new Binding(nameof(FontFamily));
        _bindFontStyle = new Binding(nameof(FontStyle));
        _bindFill = new Binding(nameof(Fill));
        _bindStroke = new Binding(nameof(Stroke));
        _bindStrokeThicknessInitial = new Binding(nameof(StrokeThicknessInitial));
    }

    public static readonly RoutedEvent WordClickedEvent =
        EventManager.RegisterRoutedEvent(nameof(WordClicked), RoutingStrategy.Bubble, typeof(WordClickedEventHandler), typeof(SelectableSubtitleText));

    public event WordClickedEventHandler WordClicked
    {
        add => AddHandler(WordClickedEvent, value);
        remove => RemoveHandler(WordClickedEvent, value);
    }

    public event EventHandler? WordClickedDown;

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(nameof(Text), typeof(string), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(string.Empty, OnTextChanged));

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    private string _textFix = string.Empty;

    public static readonly DependencyProperty IsTranslatedProperty =
        DependencyProperty.Register(nameof(IsTranslated), typeof(bool), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(false));

    public bool IsTranslated
    {
        get => (bool)GetValue(IsTranslatedProperty);
        set => SetValue(IsTranslatedProperty, value);
    }

    public static readonly DependencyProperty TextLanguageProperty =
        DependencyProperty.Register(nameof(TextLanguage), typeof(Language), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(null, OnTextLanguageChanged));

    public Language? TextLanguage
    {
        get => (Language)GetValue(TextLanguageProperty);
        set => SetValue(TextLanguageProperty, value);
    }

    public static readonly DependencyProperty SubIndexProperty =
        DependencyProperty.Register(nameof(SubIndex), typeof(int), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(0));

    public int SubIndex
    {
        get => (int)GetValue(SubIndexProperty);
        set => SetValue(SubIndexProperty, value);
    }

    public static readonly DependencyProperty FillProperty =
        OutlinedTextBlock.FillProperty.AddOwner(typeof(SelectableSubtitleText));

    public Brush Fill
    {
        get => (Brush)GetValue(FillProperty);
        set => SetValue(FillProperty, value);
    }

    public static readonly DependencyProperty StrokeProperty =
        OutlinedTextBlock.StrokeProperty.AddOwner(typeof(SelectableSubtitleText));

    public Brush Stroke
    {
        get => (Brush)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }

    public static readonly DependencyProperty WidthPercentageProperty =
        DependencyProperty.Register(nameof(WidthPercentage), typeof(double), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(60.0, OnWidthPercentageChanged));

    public double WidthPercentage
    {
        get => (double)GetValue(WidthPercentageProperty);
        set => SetValue(WidthPercentageProperty, value);
    }

    public static readonly DependencyProperty WidthPercentageFixProperty =
        DependencyProperty.Register(nameof(WidthPercentageFix), typeof(double), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(60.0));

    public double WidthPercentageFix
    {
        get => (double)GetValue(WidthPercentageFixProperty);
        set => SetValue(WidthPercentageFixProperty, value);
    }

    public static readonly DependencyProperty IgnoreLineBreakProperty =
        DependencyProperty.Register(nameof(IgnoreLineBreak), typeof(bool), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(false));

    public double MaxFixedWidth
    {
        get => (double)GetValue(MaxFixedWidthProperty);
        set => SetValue(MaxFixedWidthProperty, value);
    }

    public static readonly DependencyProperty MaxFixedWidthProperty =
        DependencyProperty.Register(nameof(MaxFixedWidth), typeof(double), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(0.0));

    public bool IgnoreLineBreak
    {
        get => (bool)GetValue(IgnoreLineBreakProperty);
        set => SetValue(IgnoreLineBreakProperty, value);
    }

    public static readonly DependencyProperty StrokeThicknessInitialProperty =
        DependencyProperty.Register(nameof(StrokeThicknessInitial), typeof(double), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(3.0));

    public double StrokeThicknessInitial
    {
        get => (double)GetValue(StrokeThicknessInitialProperty);
        set => SetValue(StrokeThicknessInitialProperty, value);
    }

    public static readonly DependencyProperty WordHoverBorderBrushProperty =
        DependencyProperty.Register(nameof(WordHoverBorderBrush), typeof(Brush), typeof(SelectableSubtitleText), new FrameworkPropertyMetadata(Brushes.Cyan));

    public Brush WordHoverBorderBrush
    {
        get => (Brush)GetValue(WordHoverBorderBrushProperty);
        set => SetValue(WordHoverBorderBrushProperty, value);
    }

    private static void OnWidthPercentageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (SelectableSubtitleText)d;
        ctl.WidthPercentageFix = (double)e.NewValue;
    }

    private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (SelectableSubtitleText)d;
        ctl.SetText((string)e.NewValue);
    }

    private static void OnTextLanguageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (SelectableSubtitleText)d;
        if (ctl.TextLanguage != null && ctl.TextLanguage.IsRTL)
        {
            ctl.wrapPanel.FlowDirection = FlowDirection.RightToLeft;
        }
        else
        {
            ctl.wrapPanel.FlowDirection = FlowDirection.LeftToRight;
        }
    }

    // SelectableTextBox uses char.IsPunctuation(), so use a regular expression for it.
    // TODO: L: Sharing the code with TextBox
    [GeneratedRegex(@"((?:[^\P{P}'-]+|\s))")]
    private static partial Regex WordSplitReg { get; }

    [GeneratedRegex(@"^(?:[^\P{P}'-]+|\s)$")]
    private static partial Regex WordSplitFullReg { get; }

    [GeneratedRegex(@"((?:[^\P{P}'-]+|\s|[\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}]))")]
    private static partial Regex ChineseWordSplitReg { get; }

    private static readonly Lazy<MeCabIpaDicTagger> MeCabTagger = new(() => MeCabIpaDicTagger.Create(), true);

    private void SetText(string text)
    {
        if (text == null)
        {
            return;
        }

        if (IgnoreLineBreak)
        {
            text = SubtitleTextUtil.FlattenText(text);
        }

        _textFix = text;

        bool containLineBreak = text.AsSpan().ContainsAny('\r', '\n');

        // If it contains line feeds, expand them to the full screen width (respecting the formatting in the SRT subtitle)
        WidthPercentageFix = containLineBreak ? 100.0 : WidthPercentage;

        wrapPanel.Children.Clear();
        _wordStart = null;

        string[] lines = text.SplitToLines().ToArray();

        var wordOffset = 0;

        // Use an OutlinedTextBlock for each word to display the border Text and enclose it in a WrapPanel
        for (int i = 0; i < lines.Length; i++)
        {
            IEnumerable<string> words;

            if (TextLanguage != null && TextLanguage.ISO6391 == "ja")
            {
                // word segmentation for Japanese
                // TODO: L: Also do word segmentation in sidebar
                var nodes = MeCabTagger.Value.Parse(lines[i]);
                List<string> wordsList = new(nodes.Length);
                foreach (var node in nodes)
                {
                    // If there are space-separated characters, such as English, add them manually since they are not on the Surface
                    if (char.IsWhiteSpace(lines[i][node.BPos]))
                    {
                        wordsList.Add(" ");
                    }
                    wordsList.Add(node.Surface);
                }

                words = wordsList;
            }
            else if (TextLanguage != null && TextLanguage.ISO6391 == "zh")
            {
                words = ChineseWordSplitReg.Split(lines[i]);
            }
            else
            {
                words = WordSplitReg.Split(lines[i]);
            }

            foreach (string word in words)
            {
                // skip empty string because Split includes
                if (word.Length == 0)
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(word))
                {
                    // Blanks are inserted with TextBlock.
                    TextBlock space = new()
                    {
                        Text = word,
                        // Created a click judgment to prevent playback toggling when clicking between words.
                        Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)),
                    };
                    space.SetBinding(TextBlock.FontSizeProperty, _bindFontSize);
                    space.SetBinding(TextBlock.FontWeightProperty, _bindFontWeight);
                    space.SetBinding(TextBlock.FontStyleProperty, _bindFontStyle);
                    space.SetBinding(TextBlock.FontFamilyProperty, _bindFontFamily);
                    wrapPanel.Children.Add(space);
                    wordOffset += word.Length;
                    continue;
                }

                bool isSplitChar = WordSplitFullReg.IsMatch(word);

                OutlinedTextBlock textBlock = new()
                {
                    Text = word,
                    ClipToBounds = false,
                    TextWrapping = TextWrapping.Wrap,
                    StrokePosition = StrokePosition.Outside,
                    IsHitTestVisible = false,
                    WordOffset = wordOffset,
                    // Fixed because the word itself is inverted
                    FlowDirection = FlowDirection.LeftToRight
                };

                wordOffset += word.Length;

                textBlock.SetBinding(OutlinedTextBlock.FontSizeProperty, _bindFontSize);
                textBlock.SetBinding(OutlinedTextBlock.FontWeightProperty, _bindFontWeight);
                textBlock.SetBinding(OutlinedTextBlock.FontStyleProperty, _bindFontStyle);
                textBlock.SetBinding(OutlinedTextBlock.FontFamilyProperty, _bindFontFamily);
                textBlock.SetBinding(OutlinedTextBlock.FillProperty, _bindFill);
                textBlock.SetBinding(OutlinedTextBlock.StrokeProperty, _bindStroke);
                textBlock.SetBinding(OutlinedTextBlock.StrokeThicknessInitialProperty, _bindStrokeThicknessInitial);

                if (isSplitChar)
                {
                    wrapPanel.Children.Add(textBlock);
                }
                else
                {
                    Border border = new()
                    {
                        // Set brush to Border because OutlinedTextBlock's character click judgment is only on the character.
                        //ref: https://stackoverflow.com/questions/50653308/hit-testing-a-transparent-element-in-a-transparent-window
                        Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)),
                        BorderThickness = new Thickness(1),
                        IsHitTestVisible = true,
                        Child = textBlock,
                        Cursor = Cursors.Hand,
                    };

                    border.MouseLeftButtonDown += WordMouseLeftButtonDown;
                    border.MouseLeftButtonUp += WordMouseLeftButtonUp;
                    border.MouseRightButtonUp += WordMouseRightButtonUp;
                    border.MouseUp += WordMouseMiddleButtonUp;

                    // Change background color on mouse over
                    border.MouseEnter += (_, _) =>
                    {
                        border.BorderBrush = WordHoverBorderBrush;
                        border.Background = new SolidColorBrush(Color.FromArgb(80, 127, 127, 127));
                    };
                    border.MouseLeave += (_, _) =>
                    {
                        border.BorderBrush = null;
                        border.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
                    };

                    wrapPanel.Children.Add(border);
                }
            }

            if (containLineBreak && i != lines.Length - 1)
            {
                // Add line breaks except at the end when there are two or more lines
                wrapPanel.Children.Add(new NewLine());
            }
        }
    }

    private void WordMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (sender is Border { Child: OutlinedTextBlock word })
        {
            _wordStart = word;
            WordClickedDown?.Invoke(this, EventArgs.Empty);

            e.Handled = true;
        }
    }

    private void WordMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (sender is Border { Child: OutlinedTextBlock word })
        {
            if (_wordStart == word)
            {
                // word clicked
                Point wordPoint = word.TranslatePoint(default, Root);

                WordClickedEventArgs args = new(WordClickedEvent)
                {
                    Mouse = MouseClick.Left,
                    Words = word.Text,
                    IsWord = true,
                    Text = _textFix,
                    IsTranslated = IsTranslated,
                    SubIndex = SubIndex,
                    WordOffset = word.WordOffset,
                    WordsX = wordPoint.X,
                    WordsWidth = word.ActualWidth
                };
                RaiseEvent(args);
            }
            else if (_wordStart != null)
            {
                // phrase selected
                var wordStart = _wordStart!;
                var wordEnd = word;

                // support right to left drag
                if (wordStart.WordOffset > wordEnd.WordOffset)
                {
                    (wordStart, wordEnd) = (wordEnd, wordStart);
                }

                List<FrameworkElement> elements = wrapPanel.Children.OfType<FrameworkElement>().ToList();

                int startIndex = elements.IndexOf((FrameworkElement)wordStart.Parent);
                int endIndex = elements.IndexOf((FrameworkElement)wordEnd.Parent);

                if (startIndex == -1 || endIndex == -1)
                {
                    Debug.Assert(startIndex >= 0);
                    Debug.Assert(endIndex >= 0);
                    return;
                }

                var selectedElements = elements[startIndex..(endIndex + 1)];
                var selectedWords = selectedElements.Select(fe =>
                {
                    switch (fe)
                    {
                        case Border { Child: OutlinedTextBlock word }:
                            return word.Text;
                        case OutlinedTextBlock splitter:
                            return splitter.Text;
                        case TextBlock space:
                            return space.Text;
                        case NewLine:
                            // convert to space
                            return " ";
                        default:
                            throw new InvalidOperationException();
                    }
                }).ToList();
                string selectedText = string.Join(string.Empty, selectedWords);

                Point startPoint = wordStart.TranslatePoint(default, Root);
                Point endPoint = wordEnd.TranslatePoint(default, Root);

                // if different Y axis, then line break or text wrapping
                double wordsX = 0;
                double wordsWidth = wrapPanel.ActualWidth;

                if (startPoint.Y == endPoint.Y)
                {
                    // selection in one line

                    if (wrapPanel.FlowDirection == FlowDirection.LeftToRight)
                    {
                        wordsX = startPoint.X;
                        wordsWidth = endPoint.X + wordEnd.ActualWidth - startPoint.X;
                    }
                    else
                    {
                        wordsX = endPoint.X;
                        wordsWidth = startPoint.X + wordStart.ActualWidth - endPoint.X;
                    }
                }

                WordClickedEventArgs args = new(WordClickedEvent)
                {
                    Mouse = MouseClick.Left,
                    Words = selectedText,
                    IsWord = false,
                    Text = _textFix,
                    IsTranslated = IsTranslated,
                    SubIndex = SubIndex,
                    WordOffset = wordStart.WordOffset,
                    WordsX = wordsX,
                    WordsWidth = wordsWidth
                };
                RaiseEvent(args);
            }

            _wordStart = null;
            e.Handled = true;
        }
    }

    private void WordMouseRightButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (sender is Border { Child: OutlinedTextBlock word })
        {
            Point wordPoint = word.TranslatePoint(default, Root);

            WordClickedEventArgs args = new(WordClickedEvent)
            {
                Mouse = MouseClick.Right,
                Words = word.Text,
                IsWord = true,
                Text = _textFix,
                IsTranslated = IsTranslated,
                SubIndex = SubIndex,
                WordOffset = word.WordOffset,
                WordsX = wordPoint.X,
                WordsWidth = word.ActualWidth
            };
            RaiseEvent(args);
            e.Handled = true;
        }
    }

    private void WordMouseMiddleButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Middle)
        {
            WordClickedEventArgs args = new(WordClickedEvent)
            {
                Mouse = MouseClick.Middle,
                Words = _textFix,
                IsWord = false,
                Text = _textFix,
                IsTranslated = IsTranslated,
                SubIndex = SubIndex,
                WordOffset = 0,
                WordsX = 0,
                WordsWidth = wrapPanel.ActualWidth
            };
            RaiseEvent(args);
            e.Handled = true;
        }
    }
}

public class NewLine : FrameworkElement
{
}

using System.Diagnostics;
using System.IO;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;

namespace LLPlayer.ViewModels;

public class SubtitlesExportDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public SubtitlesExportDialogVM(FlyleafManager fl)
    {
        FL = fl;

        IsUtf8Bom = FL.Config.Subs.SubsExportUTF8WithBom;
    }

    public List<int> SubIndexList { get; } = [0, 1];
    public int SelectedSubIndex
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(SubManager));
            }
        }
    }

    public SubManager SubManager => FL.Player.SubtitlesManager[SelectedSubIndex];

    public bool IsUtf8Bom { get; set => Set(ref field, value); }

    public TranslateExportOptions SelectedTranslateOpts { get; set => Set(ref field, value); }

    [field: AllowNull, MaybeNull]
    public DelegateCommand CmdExport => field ??= new(() =>
    {
        var playlist = FL.Player.Playlist.Selected;
        if (playlist == null)
        {
            return;
        }

        List<SubtitleLine> lines = FL.Player.SubtitlesManager[SelectedSubIndex].Subs
            .Where(s =>
            {
                if (!s.IsText)
                {
                    return false;
                }

                if (SelectedTranslateOpts == TranslateExportOptions.TranslatedOnly)
                {
                    return s.IsTranslated;
                }

                return true;
            })
            .Select(s => new SubtitleLine()
            {
                Text = (SelectedTranslateOpts != TranslateExportOptions.Original
                    ? s.DisplayText : s.Text)!,
                Start = s.StartTime,
                End = s.EndTime
            }).ToList();

        if (lines.Count == 0)
        {
            ErrorDialogHelper.ShowKnownErrorPopup("There were no subtitles to export.", "Export");
            return;
        }

        string? initDir = null;
        string fileName;

        if (FL.Player.Playlist.Url != null && File.Exists(playlist.Url))
        {
            fileName = Path.GetFileNameWithoutExtension(playlist.Url);

            // If there is currently an open file, set that folder as the base folder
            initDir = Path.GetDirectoryName(playlist.Url);
        }
        else
        {
            // If live video, use title instead
            fileName = playlist.Title;
        }

        SaveFileDialog saveFileDialog = new()
        {
            Filter = "SRT files (*.srt)|*.srt|All files (*.*)|*.*",
            FileName = fileName + ".srt",
            InitialDirectory = initDir
        };

        if (SelectedTranslateOpts != TranslateExportOptions.Original)
        {
            saveFileDialog.FileName = fileName + ".translated.srt";
        }

        if (saveFileDialog.ShowDialog() == true)
        {
            SrtExporter.ExportSrt(lines, saveFileDialog.FileName, new UTF8Encoding(IsUtf8Bom));

            // open saved file in explorer
            Process.Start("explorer.exe", $@"/select,""{saveFileDialog.FileName}""");
        }
    });

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); }
        = $"Subtitles Exporter - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 350;
    public double WindowHeight { get; set => Set(ref field, value); } = 240;

    public DialogCloseListener RequestClose { get; }

    public bool CanCloseDialog()
    {
        return true;
    }
    public void OnDialogClosed() { }

    public void OnDialogOpened(IDialogParameters parameters) { }
    #endregion

    public enum TranslateExportOptions
    {
        Original,
        TranslatedOnly,
        TranslatedWithFallback
    }
}

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using CliWrap;
using CliWrap.Builders;
using CliWrap.EventStream;
using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaStream;
using Whisper.net;
using Whisper.net.LibraryLoader;
using Whisper.net.Logger;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

#nullable enable

// TODO: L: Pause and resume ASR

/// <summary>
/// Running ASR from a media file
/// </summary>
/// <remarks>
/// Read in a separate thread from the video playback.
/// Note that multiple threads cannot seek to multiple locations for a single AVFormatContext,
/// so it is necessary to open it with another avformat_open_input for the same video.
/// </remarks>
public class SubtitlesASR
{
    private readonly SubtitlesManager _subtitlesManager;
    private readonly Config _config;
    private readonly Lock _locker = new();
    private readonly Lock _lockerSubs = new();
    private CancellationTokenSource? _cts = null;
    public HashSet<int> SubIndexSet { get; } = new();

    private readonly LogHandler Log;

    public SubtitlesASR(SubtitlesManager subtitlesManager, Config config)
    {
        _subtitlesManager = subtitlesManager;
        _config = config;

        Log = new LogHandler(("[#1]").PadRight(8, ' ') + " [SubtitlesASR  ] ");
    }

    /// <summary>
    /// Check that ASR is executable
    /// </summary>
    /// <param name="err">error information</param>
    /// <returns></returns>
    public bool CanExecute(out string err)
    {
        if (_config.Subtitles.ASREngine == SubASREngineType.WhisperCpp)
        {
            if (_config.Subtitles.WhisperCppConfig.Model == null)
            {
                err = "whisper.cpp model is not set. Please download it from the settings.";
                return false;
            }

            if (!File.Exists(_config.Subtitles.WhisperCppConfig.Model.ModelFilePath))
            {
                err = $"whisper.cpp model file '{_config.Subtitles.WhisperCppConfig.Model.ModelFileName}' does not exist in the folder. Please download it from the settings.";
                return false;
            }
        }
        else if (_config.Subtitles.ASREngine == SubASREngineType.FasterWhisper)
        {
            if (_config.Subtitles.FasterWhisperConfig.UseManualEngine)
            {
                if (!File.Exists(_config.Subtitles.FasterWhisperConfig.ManualEnginePath))
                {
                    err = "faster-whisper engine does not exist in the manual path.";
                    return false;
                }
            }
            else
            {
                if (!File.Exists(FasterWhisperConfig.DefaultEnginePath))
                {
                    err = "faster-whisper engine is not downloaded. Please download it from the settings.";
                    return false;
                }
            }

            if (_config.Subtitles.FasterWhisperConfig.UseManualModel)
            {
                if (!Directory.Exists(_config.Subtitles.FasterWhisperConfig.ManualModelDir))
                {
                    err = "faster-whisper manual model directory does not exist.";
                    return false;
                }
            }
        }

        err = "";

        return true;
    }

    /// <summary>
    /// Open media file and read all subtitle data from audio
    /// </summary>
    /// <param name="subIndex">0: Primary, 1: Secondary</param>
    /// <param name="url">media file path</param>
    /// <param name="streamIndex">Audio streamIndex</param>
    /// <param name="type">Demuxer type</param>
    /// <param name="curTime">Current playback timestamp, from which whisper is run</param>
    /// <returns>true: process completed, false: run in progress</returns>
    public bool Execute(int subIndex, string url, int streamIndex, MediaType type, TimeSpan curTime)
    {
        // When Dual ASR: Copy the other ASR result and return early
        if (SubIndexSet.Count > 0 && !SubIndexSet.Contains(subIndex))
        {
            lock (_lockerSubs)
            {
                SubIndexSet.Add(subIndex);
                int otherIndex = (subIndex + 1) % 2;

                if (_subtitlesManager[otherIndex].Subs.Count > 0)
                {
                    bool enableTranslated = _config.Subtitles[subIndex].EnabledTranslated;

                    // Copy other ASR result
                    _subtitlesManager[subIndex]
                        .Load(_subtitlesManager[otherIndex].Subs.Select(s =>
                        {
                            SubtitleData clone = s.Clone();

                            if (!enableTranslated)
                            {
                                clone.TranslatedText = null;
                                clone.EnabledTranslated = true;
                            }

                            return clone;
                        }));

                    if (!_subtitlesManager[otherIndex].IsLoading)
                    {
                        // Copy the language source if one of them is already done.
                        _subtitlesManager[subIndex].LanguageSource = _subtitlesManager[otherIndex].LanguageSource;
                    }
                }
            }

            // return early
            return false;
        }

        // If it has already been executed, cancel it to start over from the current playback position.
        if (SubIndexSet.Contains(subIndex))
        {
            Dictionary<int, List<SubtitleData>> prevSubs = new();
            HashSet<int> prevSubIndexSet = [.. SubIndexSet];
            lock (_lockerSubs)
            {
                // backup current result
                foreach (int i in SubIndexSet)
                {
                    prevSubs[i] = _subtitlesManager[i].Subs.ToList();
                }
            }
            // Cancel preceding execution and wait
            TryCancel(true);

            // restore previous result
            lock (_lockerSubs)
            {
                foreach (int i in prevSubIndexSet)
                {
                    _subtitlesManager[i].Load(prevSubs[i]);
                    // Re-enable spinner
                    _subtitlesManager[i].StartLoading();

                    SubIndexSet.Add(i);
                }
            }
        }

        lock (_locker)
        {
            SubIndexSet.Add(subIndex);

            _cts = new CancellationTokenSource();
            using AudioReader reader = new(_config, subIndex);
            reader.Open(url, streamIndex, type, _cts.Token);

            if (_cts.Token.IsCancellationRequested)
            {
                return true;
            }

            reader.ReadAll(curTime, data =>
            {
                if (_cts.Token.IsCancellationRequested)
                {
                    return;
                }

                lock (_lockerSubs)
                {
                    foreach (int i in SubIndexSet)
                    {
                        bool isInit = false;
                        if (_subtitlesManager[i].LanguageSource == null)
                        {
                            isInit = true;

                            // Delete subtitles after the first subtitle to be added (leave the previous one)
                            _subtitlesManager[i].DeleteAfter(data.StartTime);

                            // Set language
                            // Can currently only be set for the whole, not per subtitle
                            _subtitlesManager[i].LanguageSource = Language.Get(data.Language);
                        }

                        SubtitleData sub = new()
                        {
                            Text = data.Text,
                            StartTime = data.StartTime,
                            EndTime = data.EndTime,
#if DEBUG
                            ChunkNo = data.ChunkNo,
                            StartTimeChunk = data.StartTimeChunk,
                            EndTimeChunk = data.EndTimeChunk,
#endif
                        };

                        _subtitlesManager[i].Add(sub);
                        if (isInit)
                        {
                            _subtitlesManager[i].SetCurrentTime(new TimeSpan(_config.Subtitles.player.CurTime));
                        }
                    }
                }
            }, _cts.Token);

            if (!_cts.Token.IsCancellationRequested)
            {
                // TODO: L: Notify, express completion in some way
                Utils.PlayCompletionSound();
            }

            foreach (int i in SubIndexSet)
            {
                lock (_lockerSubs)
                {
                    // Stop spinner (required when dual ASR)
                    _subtitlesManager[i].StartLoading().Dispose();
                }
            }
        }

        return true;
    }

    public void TryCancel(bool isWait)
    {
        var cts = _cts;
        if (cts != null)
        {
            if (!cts.IsCancellationRequested)
            {
                lock (_lockerSubs)
                {
                    foreach (var i in SubIndexSet)
                    {
                        _subtitlesManager[i].Clear();
                    }
                }

                cts.Cancel();
                lock (_lockerSubs)
                {
                    SubIndexSet.Clear();
                }
            }
            else
            {
                Log.Info("Already cancel requested");
            }

            if (!isWait)
            {
                return;
            }

            lock (_locker)
            {
                // dispose after it is no longer used.
                cts.Dispose();
                _cts = null;
            }
        }
    }

    public void Reset(int subIndex)
    {
        if (!SubIndexSet.Contains(subIndex))
            return;

        if (SubIndexSet.Count == 2)
        {
            lock (_lockerSubs)
            {
                // When Dual ASR: only the state is cleared without stopping ASR execution.
                SubIndexSet.Remove(subIndex);
                _subtitlesManager[subIndex].Clear();
            }

            return;
        }

        // cancel asynchronously as it takes time to cancel.
        TryCancel(false);
    }
}

public class AudioReader : IDisposable
{
    private readonly Config _config;
    private readonly int _subIndex;

    private Demuxer? _demuxer;
    private AudioDecoder? _decoder;
    private AudioStream? _stream;

    private unsafe AVPacket* _packet = null;
    private unsafe AVFrame* _frame = null;
    private unsafe SwrContext* _swrContext = null;

    private bool _isFile;

    private readonly LogHandler Log;

    public AudioReader(Config config, int subIndex)
    {
        _config = config;
        _subIndex = subIndex;
        Log = new LogHandler(("[#1]").PadRight(8, ' ') + " [AudioReader   ] ");
    }

    public void Open(string url, int streamIndex, MediaType type, CancellationToken token)
    {
        _demuxer = new Demuxer(_config.Demuxer, type, _subIndex + 1, false);

        token.Register(() =>
        {
            if (_demuxer != null)
                _demuxer.Interrupter.ForceInterrupt = 1;
        });

        _demuxer.Log.Prefix = _demuxer.Log.Prefix.Replace("Demuxer: ", "DemuxerA:");
        string? error = _demuxer.Open(url);

        if (error != null)
        {
            if (token.IsCancellationRequested)
                return;

            throw new InvalidOperationException($"demuxer open error: {error}");
        }

        _stream = (AudioStream)_demuxer.AVStreamToStream[streamIndex];

        _decoder = new AudioDecoder(_config, _subIndex + 1);
        _decoder.Log.Prefix = _decoder.Log.Prefix.Replace("Decoder: ", "DecoderA:");

        error = _decoder.Open(_stream);

        if (error != null)
        {
            if (token.IsCancellationRequested)
                return;

            throw new InvalidOperationException($"decoder open error: {error}");
        }

        _isFile = File.Exists(url);
    }

    private record struct AudioChunk(MemoryStream Stream, int ChunkNumber, TimeSpan Start, TimeSpan End);

    /// <summary>
    /// Extract audio files in WAV format and run Whisper
    /// </summary>
    /// <param name="curTime">Current playback timestamp, from which whisper is run</param>
    /// <param name="addSub">Action to process one result</param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    /// <exception cref="OperationCanceledException"></exception>
    public void ReadAll(TimeSpan curTime, Action<SubtitleASRData> addSub, CancellationToken cancellationToken)
    {
        if (_demuxer == null || _decoder == null || _stream == null)
            throw new InvalidOperationException("Open() is not called");

        // Assume a network stream and parallelize the reading of packets and the execution of whisper.
        // For network video, increase capacity as downloads may take longer.
        // (concern that memory usage will increase by three times the chunk size)
        int capacity = _isFile ? 1 : 2;
        BoundedChannelOptions channelOptions = new(capacity)
        {
            SingleReader = true,
            SingleWriter = true,
        };
        Channel<AudioChunk> channel = Channel.CreateBounded<AudioChunk>(channelOptions);

        // own cancellation for producer/consumer
        var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        CancellationToken token = cts.Token;

        ConcurrentStack<MemoryStream> memoryStreamPool = new();

        // Consumer: Run whisper
        Task consumerTask = Task.Run(DoConsumer, token);

        // Producer: Extract WAV and pass to consumer
        Task producerTask = Task.Run(DoProducer, token);

        // complete channel
        producerTask.ContinueWith(t =>
            channel.Writer.Complete(), token);

        // When an exception occurs in both consumer and producer, the other is canceled.
        consumerTask.ContinueWith(t =>
            cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
        producerTask.ContinueWith(t =>
            cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);

        try
        {
            Task.WhenAll(consumerTask, producerTask).Wait();
        }
        catch (AggregateException)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                // canceled by caller
                if (CanDebug) Log.Debug("Whisper canceled");
                return;
            }

            // canceled because of exceptions
            throw;
        }

        return;

        async Task DoConsumer()
        {
            await using IASRService asrService = _config.Subtitles.ASREngine switch
            {
                SubASREngineType.WhisperCpp => new WhisperCppASRService(_config),
                SubASREngineType.FasterWhisper => new FasterWhisperASRService(_config),
                _ => throw new InvalidOperationException()
            };

            while (await channel.Reader.WaitToReadAsync(token))
            {
                // Use TryPeek() to reduce the channel capacity by one.
                if (!channel.Reader.TryPeek(out AudioChunk chunk))
                    throw new InvalidOperationException("can not peek AudioChunk from channel");

                try
                {
                    if (CanDebug) Log.Debug(
                            $"Reading chunk from channel (chunkNo: {chunk.ChunkNumber}, start: {chunk.Start}, end: {chunk.End})");

                    //// Output wav file for debugging
                    //await using (FileStream fs = new($"subtitlewhisper-{chunk.ChunkNumber}.wav", FileMode.Create, FileAccess.Write))
                    //{
                    //    chunk.Stream.WriteTo(fs);
                    //    chunk.Stream.Position = 0;
                    //}

                    await foreach (var data in asrService.Do(chunk.Stream, token))
                    {
                        TimeSpan start = chunk.Start.Add(data.start);
                        TimeSpan end = chunk.Start.Add(data.end);
                        if (end > chunk.End)
                        {
                            // Shorten by 20 ms to prevent the next subtitle from being covered
                            end = chunk.End.Subtract(TimeSpan.FromMilliseconds(20));
                        }

                        SubtitleASRData subData = new()
                        {
                            Text = data.text,
                            Language = data.language,
                            StartTime = start,
                            EndTime = end,
#if DEBUG
                            ChunkNo = chunk.ChunkNumber,
                            StartTimeChunk = chunk.Start,
                            EndTimeChunk = chunk.End
#endif
                        };

                        if (CanDebug) Log.Debug(string.Format("{0}->{1} ({2}->{3}): {4}",
                            start, end,
                            chunk.Start, chunk.End,
                            data.text));

                        addSub(subData);
                    }
                }
                finally
                {
                    chunk.Stream.SetLength(0);
                    memoryStreamPool.Push(chunk.Stream);

                    if (!channel.Reader.TryRead(out _))
                        throw new InvalidOperationException("can not discard AudioChunk from channel");
                }
            }
        }

        unsafe void DoProducer()
        {
            _packet = av_packet_alloc();
            _frame = av_frame_alloc();

            // Whisper from the current playback position
            // TODO: L: Fold back and allow the first half to run as well.
            if (curTime > TimeSpan.FromSeconds(30))
            {
                // copy from DecoderContext.CalcSeekTimestamp()
                long startTime = _demuxer.hlsCtx == null ? _demuxer.StartTime : _demuxer.hlsCtx->first_timestamp * 10;
                long ticks = curTime.Ticks + startTime;

                bool forward = false;

                if (_demuxer.Type == MediaType.Audio) ticks -= _config.Audio.Delay;

                if (ticks < startTime)
                {
                    ticks = startTime;
                    forward = true;
                }
                else if (ticks > startTime + _demuxer.Duration - (50 * 10000))
                {
                    ticks = Math.Max(startTime, startTime + _demuxer.Duration - (50 * 10000));
                    forward = false;
                }

                _ = _demuxer.Seek(ticks, forward);
            }

            // When passing the audio file to Whisper, it must be converted to a 16000 sample rate WAV file.
            // For this purpose, the ffmpeg API is used to perform the conversion.
            // Audio files are divided by a certain size, stored in memory, and passed by memory stream.
            int targetSampleRate = 16000;
            int targetChannel = 1;
            MemoryStream waveStream = new(); // MemoryStream does not need to be disposed for releasing memory
            TimeSpan waveDuration = TimeSpan.Zero; // for logging

            const int waveHeaderSize = 44;

            // Stream processing is performed by dividing the audio by a certain size and passing it to whisper.
            long chunkSize = _config.Subtitles.ASRChunkSize;
            // Also split by elapsed seconds for live
            TimeSpan chunkElapsed = TimeSpan.FromSeconds(_config.Subtitles.ASRChunkSeconds);
            Stopwatch chunkSw = new();
            chunkSw.Start();

            WriteWavHeader(waveStream, targetSampleRate, targetChannel);

            int chunkCnt = 0;
            TimeSpan? chunkStart = null;
            long framePts = AV_NOPTS_VALUE;

            int demuxErrors = 0;
            int decodeErrors = 0;

            while (!token.IsCancellationRequested)
            {
                _demuxer.Interrupter.ReadRequest();
                int ret = av_read_frame(_demuxer.fmtCtx, _packet);

                if (ret != 0)
                {
                    av_packet_unref(_packet);

                    if (_demuxer.Interrupter.Timedout)
                    {
                        if (token.IsCancellationRequested)
                            break;

                        ret.ThrowExceptionIfError("av_read_frame (timed out)");
                    }

                    if (ret == AVERROR_EOF || token.IsCancellationRequested)
                    {
                        break;
                    }

                    // demux error
                    if (CanWarn) Log.Warn($"av_read_frame: {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    if (++demuxErrors == _config.Demuxer.MaxErrors)
                    {
                        ret.ThrowExceptionIfError("av_read_frame");
                    }
                    continue;
                }

                // Discard all but the selected audio stream.
                if (_packet->stream_index != _stream.StreamIndex)
                {
                    av_packet_unref(_packet);
                    continue;
                }

                ret = avcodec_send_packet(_decoder.CodecCtx, _packet);
                av_packet_unref(_packet);

                if (ret != 0)
                {
                    if (ret == AVERROR(EAGAIN))
                    {
                        // Receive_frame and send_packet both returned EAGAIN, which is an API violation.
                        ret.ThrowExceptionIfError("avcodec_send_packet (EAGAIN)");
                    }

                    // decoder error
                    if (CanWarn) Log.Warn($"avcodec_send_packet: {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    if (++decodeErrors == _config.Decoder.MaxErrors)
                    {
                        ret.ThrowExceptionIfError("avcodec_send_packet");
                    }

                    continue;
                }

                while (ret >= 0)
                {
                    ret = avcodec_receive_frame(_decoder.CodecCtx, _frame);
                    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                    {
                        break;
                    }
                    ret.ThrowExceptionIfError("avcodec_receive_frame");

                    if (_frame->best_effort_timestamp != AV_NOPTS_VALUE)
                    {
                        framePts = _frame->best_effort_timestamp;
                    }
                    else if (_frame->pts != AV_NOPTS_VALUE)
                    {
                        framePts = _frame->pts;
                    }
                    else
                    {
                        // Certain encoders sometimes cannot get pts (APE, Musepack)
                        framePts += _frame->duration;
                    }

                    waveDuration = waveDuration.Add(new TimeSpan((long)(_frame->duration * _stream.Timebase)));

                    if (chunkStart == null)
                    {
                        chunkStart = new TimeSpan((long)(framePts * _stream.Timebase) - _demuxer.StartTime);
                        if (chunkStart.Value.Ticks < 0)
                        {
                            // Correct to 0 if negative
                            chunkStart = new TimeSpan(0);
                        }
                    }

                    ResampleTo(waveStream, _frame, targetSampleRate, targetChannel);

                    // TODO: L: want it to split at the silent part
                    if (waveStream.Length >= chunkSize || chunkSw.Elapsed >= chunkElapsed)
                    {
                        TimeSpan chunkEnd = new TimeSpan((long)(framePts * _stream.Timebase) - _demuxer.StartTime);
                        chunkCnt++;

                        if (CanInfo) Log.Info(
                            $"Process chunk (chunkNo: {chunkCnt}, sizeMB: {waveStream.Length / 1024 / 1024}, duration: {waveDuration}, elapsed: {chunkSw.Elapsed})");

                        UpdateWavHeader(waveStream);

                        AudioChunk chunk = new(waveStream, chunkCnt, chunkStart.Value, chunkEnd);

                        if (CanDebug) Log.Debug($"Writing chunk to channel ({chunkCnt})");
                        // if channel capacity reached, it will be waited
                        channel.Writer.WriteAsync(chunk, token).AsTask().Wait(token);
                        if (CanDebug) Log.Debug($"Done writing chunk to channel ({chunkCnt})");

                        if (memoryStreamPool.TryPop(out var stream))
                            waveStream = stream;
                        else
                            waveStream = new MemoryStream();

                        WriteWavHeader(waveStream, targetSampleRate, targetChannel);
                        waveDuration = TimeSpan.Zero;

                        chunkStart = null;
                        chunkSw.Restart();
                        framePts = AV_NOPTS_VALUE;
                    }
                }
            }

            token.ThrowIfCancellationRequested();

            // Process remaining
            if (waveStream.Length > waveHeaderSize && framePts != AV_NOPTS_VALUE)
            {
                TimeSpan chunkEnd = new TimeSpan((long)(framePts * _stream.Timebase) - _demuxer.StartTime);

                chunkCnt++;

                if (CanInfo) Log.Info(
                    $"Process last chunk (chunkNo: {chunkCnt}, sizeMB: {waveStream.Length / 1024 / 1024}, duration: {waveDuration}, elapsed: {chunkSw.Elapsed})");

                UpdateWavHeader(waveStream);

                AudioChunk chunk = new(waveStream, chunkCnt, chunkStart!.Value, chunkEnd);

                if (CanDebug) Log.Debug($"Writing last chunk to channel ({chunkCnt})");
                channel.Writer.WriteAsync(chunk, token).AsTask().Wait(token);
                if (CanDebug) Log.Debug($"Done writing last chunk to channel ({chunkCnt})");
            }
        }
    }

    private static void WriteWavHeader(Stream stream, int sampleRate, int channels)
    {
        using BinaryWriter writer = new(stream, Encoding.UTF8, true);
        writer.Write(['R', 'I', 'F', 'F']);
        writer.Write(0); // placeholder for file size
        writer.Write(['W', 'A', 'V', 'E']);
        writer.Write(['f', 'm', 't', ' ']);
        writer.Write(16); // PCM header size
        writer.Write((short)1); // PCM format
        writer.Write((short)channels);
        writer.Write(sampleRate);
        writer.Write(sampleRate * channels * 2); // Byte rate
        writer.Write((short)(channels * 2)); // Block align
        writer.Write((short)16); // Bits per sample
        writer.Write(['d', 'a', 't', 'a']);
        writer.Write(0); // placeholder for data size
    }

    private static void UpdateWavHeader(Stream stream)
    {
        long fileSize = stream.Length;
        stream.Seek(4, SeekOrigin.Begin);
        stream.Write(BitConverter.GetBytes((int)(fileSize - 8)), 0, 4);
        stream.Seek(40, SeekOrigin.Begin);
        stream.Write(BitConverter.GetBytes((int)(fileSize - 44)), 0, 4);
        stream.Position = 0;
    }

    private byte[] _sampledBuf = [];
    private int _sampledBufSize;

    // for codec change detection
    private int _lastFormat;
    private int _lastSampleRate;
    private ulong _lastChannelLayout;

    private unsafe void ResampleTo(Stream toStream, AVFrame* frame, int targetSampleRate, int targetChannel)
    {
        bool codecChanged = false;

        if (_lastFormat != frame->format)
        {
            _lastFormat = frame->format;
            codecChanged = true;
        }
        if (_lastSampleRate != frame->sample_rate)
        {
            _lastSampleRate = frame->sample_rate;
            codecChanged = true;
        }
        if (_lastChannelLayout != frame->ch_layout.u.mask)
        {
            _lastChannelLayout = frame->ch_layout.u.mask;
            codecChanged = true;
        }

        // Reinitialize SwrContext because codec changed
        // Note that native error will occur if not reinitialized.
        // Reference: AudioDecoder::RunInternal
        if (_swrContext != null && codecChanged)
        {
            fixed (SwrContext** ptr = &_swrContext)
            {
                swr_free(ptr);
            }
            _swrContext = null;
        }

        if (_swrContext == null)
        {
            AVChannelLayout outLayout;
            av_channel_layout_default(&outLayout, targetChannel);

            // NOTE: important to reuse this context
            fixed (SwrContext** ptr = &_swrContext)
            {
                swr_alloc_set_opts2(
                    ptr,
                    &outLayout,
                    AVSampleFormat.S16,
                    targetSampleRate,
                    &frame->ch_layout,
                    (AVSampleFormat)frame->format,
                    frame->sample_rate,
                    0, null)
                    .ThrowExceptionIfError("swr_alloc_set_opts2");

                swr_init(_swrContext)
                    .ThrowExceptionIfError("swr_init");
            }
        }

        // ffmpeg ref: https://github.com/FFmpeg/FFmpeg/blob/504df09c34607967e4109b7b114ee084cf15a3ae/libavfilter/af_aresample.c#L171-L227
        double ratio = targetSampleRate * 1.0 / frame->sample_rate; // 16000:44100=0.36281179138321995
        int nOut = (int)(frame->nb_samples * ratio) + 32;

        long delay = swr_get_delay(_swrContext, targetSampleRate);
        if (delay > 0)
        {
            nOut += (int)Math.Min(delay, Math.Max(4096, nOut));
        }
        int needed = nOut * targetChannel * sizeof(ushort);

        if (_sampledBufSize < needed)
        {
            _sampledBuf = new byte[needed];
            _sampledBufSize = needed;
        }

        int samplesPerChannel;

        fixed (byte* dst = _sampledBuf)
        {
            samplesPerChannel = swr_convert(
                 _swrContext,
                 &dst,
                 nOut,
                 frame->extended_data,
                 frame->nb_samples);
        }
        samplesPerChannel.ThrowExceptionIfError("swr_convert");

        int resampledDataSize = samplesPerChannel * targetChannel * sizeof(ushort);

        toStream.Write(_sampledBuf, 0, resampledDataSize);
    }

    private bool _isDisposed;

    public unsafe void Dispose()
    {
        if (_isDisposed)
            return;

        // av_frame_alloc
        if (_frame != null)
        {
            fixed (AVFrame** ptr = &_frame)
            {
                av_frame_free(ptr);
            }
        }

        // av_packet_alloc
        if (_packet != null)
        {
            fixed (AVPacket** ptr = &_packet)
            {
                av_packet_free(ptr);
            }
        }

        // swr_init
        if (_swrContext != null)
        {
            fixed (SwrContext** ptr = &_swrContext)
            {
                swr_free(ptr);
            }
        }

        _decoder?.Dispose();
        if (_demuxer != null)
        {
            _demuxer.Interrupter.ForceInterrupt = 0;
            _demuxer.Dispose();
        }

        _isDisposed = true;
    }
}

public interface IASRService : IAsyncDisposable
{
    public IAsyncEnumerable<(string text, TimeSpan start, TimeSpan end, string language)> Do(MemoryStream waveStream, CancellationToken token);
}

// https://github.com/sandrohanea/whisper.net
// https://github.com/ggerganov/whisper.cpp
public class WhisperCppASRService : IASRService
{
    private readonly Config _config;

    private readonly LogHandler Log;
    private readonly IDisposable _logger;
    private readonly WhisperFactory _factory;
    private readonly WhisperProcessor _processor;

    private readonly bool _isLanguageDetect;
    private string? _detectedLanguage;

    public WhisperCppASRService(Config config)
    {
        _config = config;
        Log = new LogHandler(("[#1]").PadRight(8, ' ') + " [WhisperCpp    ] ");

        if (_config.Subtitles.WhisperCppConfig.RuntimeLibraries.Count >= 1)
        {
            RuntimeOptions.RuntimeLibraryOrder = [.. _config.Subtitles.WhisperCppConfig.RuntimeLibraries];
        }
        else
        {
            RuntimeOptions.RuntimeLibraryOrder = [RuntimeLibrary.Cpu, RuntimeLibrary.CpuNoAvx]; // fallback to default
        }

        _logger = CanDebug
            ? LogProvider.AddLogger((level, s) => Log.Debug($"[Whisper.net] [{level.ToString()}] {s}"))
            : Disposable.Empty;

        if (CanDebug) Log.Debug($"Selecting whisper runtime libraries from ({string.Join(",", RuntimeOptions.RuntimeLibraryOrder)})");

        _factory = WhisperFactory.FromPath(_config.Subtitles.WhisperCppConfig.Model!.ModelFilePath, _config.Subtitles.WhisperCppConfig.GetFactoryOptions());

        if (CanDebug) Log.Debug($"Selected whisper runtime library '{RuntimeOptions.LoadedLibrary}'");

        WhisperProcessorBuilder whisperBuilder = _factory.CreateBuilder();
        _processor = _config.Subtitles.WhisperCppConfig.ConfigureBuilder(_config.Subtitles.WhisperConfig, whisperBuilder).Build();

        if (_config.Subtitles.WhisperCppConfig.IsEnglishModel)
        {
            _isLanguageDetect = false;
            _detectedLanguage = "en";
        }
        else
        {
            _isLanguageDetect = _config.Subtitles.WhisperConfig.LanguageDetection;
        }
    }

    public async ValueTask DisposeAsync()
    {
        await _processor.DisposeAsync();
        _factory.Dispose();
        _logger.Dispose();
    }

    public async IAsyncEnumerable<(string text, TimeSpan start, TimeSpan end, string language)> Do(MemoryStream waveStream, [EnumeratorCancellation] CancellationToken token)
    {
        // If language detection is on, set detected language manually
        // TODO: L: Currently this is set because language information is managed for the entire subtitle,
        // but if language information is maintained for each subtitle, it should not be set.
        if (_isLanguageDetect && _detectedLanguage is not null)
        {
            _processor.ChangeLanguage(_detectedLanguage);
        }

        await foreach (var result in _processor.ProcessAsync(waveStream, token).ConfigureAwait(false))
        {
            token.ThrowIfCancellationRequested();

            if (_detectedLanguage is null && !string.IsNullOrEmpty(result.Language))
            {
                _detectedLanguage = result.Language;
            }

            string text = result.Text.Trim(); // remove leading whitespace

            yield return (text, result.Start, result.End, result.Language);
        }
    }
}

// https://github.com/Purfview/whisper-standalone-win
// Purfview's Stand-alone Faster-Whisper-XXL & Faster-Whisper
// Do not support official OpenAI Whisper version
public partial class FasterWhisperASRService : IASRService
{
    private readonly Config _config;

    public FasterWhisperASRService(Config config)
    {
        _config = config;

        _cmdBase = BuildCommand(_config.Subtitles.FasterWhisperConfig, _config.Subtitles.WhisperConfig);

        if (_config.Subtitles.FasterWhisperConfig.IsEnglishModel)
        {
            // force English and disable auto-detection
            _isLanguageDetect = false;
            _manualLanguage = "en";
        }
        else
        {
            _isLanguageDetect = _config.Subtitles.WhisperConfig.LanguageDetection;
            _manualLanguage = _config.Subtitles.WhisperConfig.Language;
        }

        if (!_config.Subtitles.FasterWhisperConfig.UseManualModel)
        {
            WhisperConfig.EnsureModelsDirectory();
        }
    }

    private readonly Command _cmdBase;
    private readonly bool _isLanguageDetect;
    private readonly string _manualLanguage;
    private string? _detectedLanguage;

    [GeneratedRegex("^Detected language '(.+)' with probability")]
    private static partial Regex LanguageReg { get; }

    [GeneratedRegex(@"^\[\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}\.\d{3}\] ")]
    private static partial Regex SubShortReg { get; } // [08:15.050 --> 08:16.450] Text

    [GeneratedRegex(@"^\[\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3}\] ")]
    private static partial Regex SubLongReg { get; } // [02:08:15.050 --> 02:08:16.450] Text

    [GeneratedRegex("^Operation finished in:")]
    private static partial Regex EndReg { get; }

    public ValueTask DisposeAsync()
    {
        return ValueTask.CompletedTask;
    }

    public static Command BuildCommand(FasterWhisperConfig config, WhisperConfig commonConfig)
    {
        string tempFolder = Path.GetTempPath();
        string enginePath = config.UseManualEngine ? config.ManualEnginePath! : FasterWhisperConfig.DefaultEnginePath;

        ArgumentsBuilder args = new();
        args.Add("--output_dir").Add(tempFolder);
        args.Add("--output_format").Add("srt");
        args.Add("--verbose").Add("True");
        args.Add("--beep_off");
        args.Add("--model").Add(config.Model);
        args.Add("--model_dir")
            .Add(config.UseManualModel ? config.ManualModelDir! : WhisperConfig.ModelsDirectory);

        if (config.IsEnglishModel)
        {
            args.Add("--language").Add("en");
        }
        else
        {
            if (commonConfig.Translate)
                args.Add("--task").Add("translate");

            if (!commonConfig.LanguageDetection)
                args.Add("--language").Add(commonConfig.Language);
        }

        string arguments = args.Build();

        if (!string.IsNullOrWhiteSpace(config.ExtraArguments))
        {
            arguments += $" {config.ExtraArguments}";
        }

        Command cmd = Cli.Wrap(enginePath)
            .WithArguments(arguments)
            .WithValidation(CommandResultValidation.None);

        if (config.ProcessPriority != ProcessPriorityClass.Normal)
        {
            cmd = cmd.WithResourcePolicy(builder =>
                builder.SetPriority(config.ProcessPriority));
        }

        return cmd;
    }

    private static TimeSpan ParseTime(ReadOnlySpan<char> time, bool isLong)
    {
        if (isLong)
        {
            // 01:28:02.130
            // hh:mm:ss.fff
            int hours = int.Parse(time[..2]);
            int minutes = int.Parse(time[3..5]);
            int seconds = int.Parse(time[6..8]);
            int milliseconds = int.Parse(time[9..12]);
            return new TimeSpan(0, hours, minutes, seconds, milliseconds);
        }
        else
        {
            // 28:02.130
            // mm:ss.fff
            int minutes = int.Parse(time[..2]);
            int seconds = int.Parse(time[3..5]);
            int milliseconds = int.Parse(time[6..9]);
            return new TimeSpan(0, 0, minutes, seconds, milliseconds);
        }
    }

    public async IAsyncEnumerable<(string text, TimeSpan start, TimeSpan end, string language)> Do(MemoryStream waveStream, [EnumeratorCancellation] CancellationToken token)
    {
        string tempFilePath = Path.GetTempFileName();
        // because no output option
        string outputFilePath = Path.ChangeExtension(tempFilePath, "srt");

        // write WAV to tmp folder
        await using (FileStream fileStream = new(tempFilePath, FileMode.Create, FileAccess.Write))
        {
            waveStream.WriteTo(fileStream);
        }

        CancellationTokenSource forceCts = new();
        token.Register(() =>
        {
            // force kill if not exited when sending interrupt
            forceCts.CancelAfter(5000);
        });

        try
        {
            string? lastLine = null;
            StringBuilder output = new(); // for error output
            Lock outputLock = new();
            bool oneSuccess = false;

            ArgumentsBuilder args = new();
            if (_isLanguageDetect && !string.IsNullOrEmpty(_detectedLanguage))
            {
                args.Add("--language").Add(_detectedLanguage);
            }
            args.Add(tempFilePath);
            string addedArgs = args.Build();

            Command cmd = _cmdBase.WithArguments($"{_cmdBase.Arguments} {addedArgs}");

            await foreach (var cmdEvent in cmd.ListenAsync(Encoding.Default, Encoding.Default, forceCts.Token, token))
            {
                token.ThrowIfCancellationRequested();

                if (cmdEvent is StandardErrorCommandEvent stdErr)
                {
                    lock (outputLock)
                    {
                        output.AppendLine(stdErr.Text);
                    }

                    continue;
                }

                if (cmdEvent is not StandardOutputCommandEvent stdOut)
                {
                    continue;
                }

                string line = stdOut.Text;

                // process stdout
                if (!oneSuccess)
                {
                    lock (outputLock)
                    {
                        output.AppendLine(line);
                    }

                }
                if (string.IsNullOrEmpty(line))
                {
                    continue;
                }

                lastLine = line;

                if (_isLanguageDetect && _detectedLanguage == null)
                {
                    var match = LanguageReg.Match(line);
                    if (match.Success)
                    {
                        string languageName = match.Groups[1].Value;
                        _detectedLanguage = WhisperLanguage.LanguageToCode[languageName];
                    }

                    continue;
                }

                bool isLong = false;

                Match subtitleMatch = SubShortReg.Match(line);
                if (!subtitleMatch.Success)
                {
                    subtitleMatch = SubLongReg.Match(line);
                    if (!subtitleMatch.Success)
                    {
                        continue;
                    }

                    isLong = true;
                }

                ReadOnlySpan<char> lineSpan = line.AsSpan();

                Range startRange = 1..10;
                Range endRange = 15..24;
                Range textRange = 26..;

                if (isLong)
                {
                    startRange = 1..13;
                    endRange = 18..30;
                    textRange = 32..;
                }

                TimeSpan start = ParseTime(lineSpan[startRange], isLong);
                TimeSpan end = ParseTime(lineSpan[endRange], isLong);
                // because some languages have leading spaces
                string text = lineSpan[textRange].Trim().ToString();

                yield return (text, start, end, _isLanguageDetect ? _detectedLanguage! : _manualLanguage);

                if (!oneSuccess)
                {
                    oneSuccess = true;
                }
            }

            // validate if success
            if (lastLine == null || !EndReg.Match(lastLine).Success)
            {
                throw new InvalidOperationException("Failed to execute faster-whisper")
                {
                    Data =
                    {
                        ["whisper_command"] = cmd.CommandToText(),
                        ["whisper_output"] = output.ToString()
                    }
                };
            }
        }
        finally
        {
            // delete tmp wave
            if (File.Exists(tempFilePath))
            {
                File.Delete(tempFilePath);
            }
            // delete output srt
            if (File.Exists(outputFilePath))
            {
                File.Delete(outputFilePath);
            }
        }
    }
}

public class SubtitleASRData
{
    public required string Text { get; init; }
    public required TimeSpan StartTime { get; init; }
    public required TimeSpan EndTime { get; init; }

#if DEBUG
    public required int ChunkNo { get; init; }
    public required TimeSpan StartTimeChunk { get; init; }
    public required TimeSpan EndTimeChunk { get; init; }
#endif

    public TimeSpan Duration => EndTime - StartTime;

    // ISO6391
    // ref: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
    public required string Language { get; init; }
}

using System.Diagnostics;
using System.IO;
using System.Net.Http;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;
using SevenZip;
using File = System.IO.File;

namespace LLPlayer.ViewModels;

public class WhisperEngineDownloadDialogVM : Bindable, IDialogAware
{
    // currently not reusable at all
    public static string EngineURL => "https://github.com/Purfview/whisper-standalone-win/releases/tag/Faster-Whisper-XXL";
    public static string EngineFile => "Faster-Whisper-XXL_r245.4_windows.7z";
    private static string EngineDownloadURL =
        "https://github.com/umlx5h/LLPlayer/releases/download/v0.0.1/Faster-Whisper-XXL_r245.4_windows.7z";
    private static string EngineName = "Faster-Whisper-XXL";
    private static string EnginePath = Path.Combine(WhisperConfig.EnginesDirectory, EngineName);

    public FlyleafManager FL { get; }

    public WhisperEngineDownloadDialogVM(FlyleafManager fl)
    {
        FL = fl;

        CmdDownloadEngine!.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == nameof(CmdDownloadEngine.IsExecuting))
            {
                OnPropertyChanged(nameof(CanDownload));
                OnPropertyChanged(nameof(CanDelete));
            }
        };
    }

    public string StatusText { get; set => Set(ref field, value); } = "";

    public long DownloadedSize { get; set => Set(ref field, value); }

    public bool Downloaded => Directory.Exists(EnginePath);

    public bool CanDownload => !Downloaded && !CmdDownloadEngine!.IsExecuting;

    public bool CanDelete => Downloaded && !CmdDownloadEngine!.IsExecuting;

    private CancellationTokenSource? _cts;

    public AsyncDelegateCommand? CmdDownloadEngine => field ??= new AsyncDelegateCommand(async () =>
    {
        _cts = new CancellationTokenSource();
        CancellationToken token = _cts.Token;

        string tempPath = Path.GetTempPath();
        string tempDownloadFile = Path.Combine(tempPath, EngineFile);

        try
        {
            StatusText = $"Engine '{EngineName}' downloading..";

            await DownloadEngineWithProgressAsync(EngineDownloadURL, tempDownloadFile, token);

            StatusText = $"Engine '{EngineName}' unzipping..";
            await UnzipEngine(tempDownloadFile);

            StatusText = $"Engine '{EngineName}' is downloaded successfully";
            OnDownloadStatusChanged();
        }
        catch (OperationCanceledException)
        {
            StatusText = "Download canceled";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to download: {ex.Message}";
        }
        finally
        {
            _cts = null;
            DeleteTempEngine();
        }

        return;

        bool DeleteTempEngine()
        {
            // Delete temporary files if they exist
            if (File.Exists(tempDownloadFile))
            {
                try
                {
                    File.Delete(tempDownloadFile);
                }
                catch (Exception)
                {
                    // ignore

                    return false;
                }
            }

            return true;
        }
    }).ObservesCanExecute(() => CanDownload);

    public DelegateCommand? CmdCancelDownloadEngine => field ??= new(() =>
    {
        _cts?.Cancel();
    });

    public AsyncDelegateCommand? CmdDeleteEngine => field ??= new AsyncDelegateCommand(async () =>
    {
        try
        {
            StatusText = $"Engine '{EngineName}' deleting...";

            // Delete engine if exists
            if (Directory.Exists(EnginePath))
            {
                await Task.Run(() =>
                {
                    Directory.Delete(EnginePath, true);
                });
            }

            OnDownloadStatusChanged();

            StatusText = $"Engine '{EngineName}' is deleted successfully";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to delete engine: {ex.Message}";
        }
    }).ObservesCanExecute(() => CanDelete);

    public DelegateCommand? CmdOpenFolder => field ??= new(() =>
    {
        if (!Directory.Exists(WhisperConfig.EnginesDirectory))
            return;

        Process.Start(new ProcessStartInfo
        {
            FileName = WhisperConfig.EnginesDirectory,
            UseShellExecute = true,
            CreateNoWindow = true
        });
    });

    private void OnDownloadStatusChanged()
    {
        OnPropertyChanged(nameof(CanDownload));
        OnPropertyChanged(nameof(CanDelete));
    }

    private async Task UnzipEngine(string zipPath)
    {
        WhisperConfig.EnsureEnginesDirectory();

        SevenZipBase.SetLibraryPath("lib/7z.dll");

        using (SevenZipExtractor extractor = new(zipPath))
        {
            await extractor.ExtractArchiveAsync(WhisperConfig.EnginesDirectory);
        }

        string licencePath = Path.Combine(WhisperConfig.EnginesDirectory, "license.txt");

        if (File.Exists(licencePath) && Directory.Exists(WhisperConfig.EnginesDirectory))
        {
            // move license.txt to engine directory
            File.Move(licencePath, Path.Combine(EnginePath, "license.txt"));
        }
    }

    private async Task<long> DownloadEngineWithProgressAsync(string url, string destinationPath, CancellationToken token)
    {
        DownloadedSize = 0;

        using HttpClient httpClient = new();
        httpClient.Timeout = TimeSpan.FromSeconds(10);

        using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);

        response.EnsureSuccessStatusCode();

        await using Stream engineStream = await response.Content.ReadAsStreamAsync(token);
        await using FileStream fileWriter = File.Open(destinationPath, FileMode.Create);

        byte[] buffer = new byte[1024 * 128];
        int bytesRead;
        long totalBytesRead = 0;

        Stopwatch sw = new();
        sw.Start();

        while ((bytesRead = await engineStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
        {
            await fileWriter.WriteAsync(buffer, 0, bytesRead, token);
            totalBytesRead += bytesRead;

            if (sw.Elapsed > TimeSpan.FromMilliseconds(50))
            {
                DownloadedSize = totalBytesRead;
                sw.Restart();
            }

            token.ThrowIfCancellationRequested();
        }

        return totalBytesRead;
    }

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); } = $"Whisper Engine Downloader - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 400;
    public double WindowHeight { get; set => Set(ref field, value); } = 210;

    public bool CanCloseDialog() => !CmdDownloadEngine!.IsExecuting;
    public void OnDialogClosed() { }
    public void OnDialogOpened(IDialogParameters parameters) { }
    public DialogCloseListener RequestClose { get; }
    #endregion IDialogAware
}

using System.Collections.Generic;
using System.Text;
using System.Threading;

using Vortice.D3DCompiler;
using Vortice.Direct3D;
using Vortice.Direct3D11;

using ID3D11Device = Vortice.Direct3D11.ID3D11Device;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaFramework.MediaRenderer;

internal class BlobWrapper
{
    public Blob blob;
    public BlobWrapper(Blob blob) => this.blob = blob;
    public BlobWrapper() { }
}

internal static class ShaderCompiler
{
    internal static Blob VSBlob = Compile(VS, false); // TODO Embedded?

    const int MAXSIZE = 64;
    static Dictionary<string, BlobWrapper> cache = new();

    internal static ID3D11PixelShader CompilePS(ID3D11Device device, string uniqueId, string hlslSample, List<string> defines = null)
    {
        BlobWrapper bw;

        lock (cache)
        {
            if (cache.Count > MAXSIZE)
            {
                Engine.Log.Trace($"[ShaderCompiler] Clears cache");
                foreach (var bw1 in cache.Values)
                    bw1.blob.Dispose();

                cache.Clear();
            }

            cache.TryGetValue(uniqueId, out var bw2);
            if (bw2 != null)
            {
                Engine.Log.Trace($"[ShaderCompiler] Found in cache {uniqueId}");
                lock(bw2)
                    return device.CreatePixelShader(bw2.blob);
            }

            bw = new();
            Monitor.Enter(bw);
            cache.Add(uniqueId, bw);
        }

        var blob = Compile(PS_HEADER + hlslSample + PS_FOOTER, true, defines);
        bw.blob = blob;
        var ps = device.CreatePixelShader(bw.blob);
        Monitor.Exit(bw);

        Engine.Log.Trace($"[ShaderCompiler] Compiled {uniqueId}");
        return ps;
    }
    internal static Blob Compile(string hlsl, bool isPS = true, List<string> defines = null)
    {
        ShaderMacro[] definesMacro = null;

        if (defines != null)
        {
            definesMacro = new ShaderMacro[defines.Count + 1];
            for(int i=0; i<defines.Count; i++)
                definesMacro[i] = new ShaderMacro() { Name = defines[i], Definition = "" };
        }

        return Compile(Encoding.UTF8.GetBytes(hlsl), isPS, definesMacro);

    }
    internal static unsafe Blob Compile(byte[] bytes, bool isPS = true, ShaderMacro[] defines = null)
    {
        string psOrvs = isPS ? "ps" : "vs";
        Compiler.Compile(bytes, defines, null, "main", null, $"{psOrvs}_4_0_level_9_3", ShaderFlags.OptimizationLevel3, out var shaderBlob, out var psError);

        if (psError != null && psError.BufferPointer != IntPtr.Zero)
        {
            string[] errors = BytePtrToStringUTF8((byte*)psError.BufferPointer).Split('\n');

            foreach (string line in errors)
                Engine.Log.Error($"[ShaderCompile] {line}");
        }

        return shaderBlob;
    }

    //private static void CompileEmbeddedShaders() // Not Used
    //{
    //    Assembly assembly = Assembly.GetExecutingAssembly();
    //    string[] shaders = assembly.GetManifestResourceNames().Where(x => GetUrlExtention(x) == "hlsl").ToArray();

    //    foreach (string shader in shaders)
    //        using (Stream stream = assembly.GetManifestResourceStream(shader))
    //        {
    //            string shaderName = shader.Substring(0, shader.Length - 5);
    //            shaderName = shaderName.Substring(shaderName.LastIndexOf('.') + 1);

    //            byte[] bytes = new byte[stream.Length];
    //            stream.Read(bytes, 0, bytes.Length);

    //            CompileShader(bytes, shaderName);
    //        }
    //}

    //private unsafe static void CompileFileShaders()
    //{
    //    List<string> shaders = Directory.EnumerateFiles(EmbeddedShadersFolder, "*.hlsl").ToList();
    //    foreach (string shader in shaders)
    //    {
    //        string shaderName = shader.Substring(0, shader.Length - 5);
    //        shaderName = shaderName.Substring(shaderName.LastIndexOf('\\') + 1);

    //        CompileShader(File.ReadAllBytes(shader), shaderName);
    //    }
    //}

    // Loads compiled blob shaders
    //private static void LoadShaders()
    //{
    //    Assembly assembly = Assembly.GetExecutingAssembly();
    //    string[] shaders = assembly.GetManifestResourceNames().Where(x => GetUrlExtention(x) == "blob").ToArray();
    //    string tempFile = Path.GetTempFileName();

    //    foreach (string shader in shaders)
    //    {
    //        using (Stream stream = assembly.GetManifestResourceStream(shader))
    //        {
    //            var shaderName = shader.Substring(0, shader.Length - 5);
    //            shaderName = shaderName.Substring(shaderName.LastIndexOf('.') + 1);

    //            byte[] bytes = new byte[stream.Length];
    //            stream.Read(bytes, 0, bytes.Length);

    //            Dictionary<string, Blob> curShaders = shaderName.Substring(0, 2).ToLower() == "vs" ? VSShaderBlobs : PSShaderBlobs;

    //            File.WriteAllBytes(tempFile, bytes);
    //            curShaders.Add(shaderName, Compiler.ReadFileToBlob(tempFile));
    //        }
    //    }
    //}

    // Should work at least from main Samples => FlyleafPlayer (WPF Control) (WPF)
    //static string EmbeddedShadersFolder = @"..\..\..\..\..\..\FlyleafLib\MediaFramework\MediaRenderer\Shaders";
    //static Assembly ASSEMBLY        = Assembly.GetExecutingAssembly();
    //static string   SHADERS_NS      = typeof(Renderer).Namespace + ".Shaders.";

    //static byte[] GetEmbeddedShaderResource(string shaderName)
    //{
    //    using (Stream stream = ASSEMBLY.GetManifestResourceStream(SHADERS_NS + shaderName + ".hlsl"))
    //    {
    //        byte[] bytes = new byte[stream.Length];
    //        stream.Read(bytes, 0, bytes.Length);

    //        return bytes;
    //    }
    //}

    const string PS_HEADER = @"
#pragma warning( disable: 3571 )
Texture2D		Texture1		: register(t0);
Texture2D		Texture2		: register(t1);
Texture2D		Texture3		: register(t2);
Texture2D		Texture4		: register(t3);

cbuffer         Config          : register(b0)
{
    int coefsIndex;
    int hdrmethod;

    float brightness;
    float contrast;

    float g_luminance;
    float g_toneP1;
    float g_toneP2;

    float texWidth;
};

SamplerState Sampler : IMMUTABLE
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = CLAMP;
    AddressV = CLAMP;
    AddressW = CLAMP;
    ComparisonFunc = NEVER;
    MinLOD = 0;
};

#if defined(YUV)
// YUV to RGB matrix coefficients
static const float4x4 coefs[] =
{
    // Limited -> Full
    {
        // BT2020 (srcBits = 10)
        { 1.16438353, 1.16438353, 1.16438353, 0 },
        { 0, -0.187326103, 2.14177227, 0 },
        { 1.67867422, -0.650424361, 0, 0 },
        { -0.915688038, 0.347458541, -1.14814520, 1 }
    },

        // BT709
    {
        { 1.16438341, 1.16438341, 1.16438341, 0 },
        { 0, -0.213248596, 2.11240149, 0 },
        { 1.79274082, -0.532909214, 0, 0 },
        { -0.972944975, 0.301482648, -1.13340211, 1 }
    },
        // BT601
    {
        { 1.16438341, 1.16438341, 1.16438341, 0 },
        { 0, -0.391762286, 2.01723194, 0 },
        { 1.59602666, -0.812967658, 0, 0 },
        { -0.874202192, 0.531667829, -1.08563077, 1 },
    }
};
#endif

#if defined(HDR)
// hdrmethod enum
static const int Aces       = 1;
static const int Hable      = 2;
static const int Reinhard   = 3;

// HDR to SDR color convert (Thanks to KODI community https://github.com/thexai/xbmc)
static const float ST2084_m1 = 2610.0f / (4096.0f * 4.0f);
static const float ST2084_m2 = (2523.0f / 4096.0f) * 128.0f;
static const float ST2084_c1 = 3424.0f / 4096.0f;
static const float ST2084_c2 = (2413.0f / 4096.0f) * 32.0f;
static const float ST2084_c3 = (2392.0f / 4096.0f) * 32.0f;

static const float4x4 bt2020tobt709color =
{
    { 1.6604f, -0.1245f, -0.0181f, 0 },
    { -0.5876f, 1.1329f, -0.10057f, 0 },
    { -0.07284f, -0.0083f, 1.1187f, 0 },
    { 0, 0, 0, 0 }
};

float3 inversePQ(float3 x)
{
    x = pow(max(x, 0.0f), 1.0f / ST2084_m2);
    x = max(x - ST2084_c1, 0.0f) / (ST2084_c2 - ST2084_c3 * x);
    x = pow(x, 1.0f / ST2084_m1);
    return x;
}

#if defined(HLG)
float3 inverseHLG(float3 x)
{
    const float B67_a = 0.17883277f;
    const float B67_b = 0.28466892f;
    const float B67_c = 0.55991073f;
    const float B67_inv_r2 = 4.0f;
    x = (x <= 0.5f) ? x * x * B67_inv_r2 : exp((x - B67_c) / B67_a) + B67_b;
    return x;
}

float3 tranferPQ(float3 x)
{
    x = pow(x / 1000.0f, ST2084_m1);
    x = (ST2084_c1 + ST2084_c2 * x) / (1.0f + ST2084_c3 * x);
    x = pow(x, ST2084_m2);
    return x;
}

#endif
float3 aces(float3 x)
{
    const float A = 2.51f;
    const float B = 0.03f;
    const float C = 2.43f;
    const float D = 0.59f;
    const float E = 0.14f;
    return (x * (A * x + B)) / (x * (C * x + D) + E);
}

float3 hable(float3 x)
{
    const float A = 0.15f;
    const float B = 0.5f;
    const float C = 0.1f;
    const float D = 0.2f;
    const float E = 0.02f;
    const float F = 0.3f;
    return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

static const float3 bt709coefs = { 0.2126f, 1.0f - 0.2126f - 0.0722f, 0.0722f };
float reinhard(float x)
{
    return x * (1.0f + x / (g_toneP1 * g_toneP1)) / (1.0f + x);
}
#endif

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texture  : TEXCOORD;
};

float4 main(PSInput input) : SV_TARGET
{
    float4 color;

    // Dynamic Sampling

";

    const string PS_FOOTER = @"

    // YUV to RGB
#if defined(YUV)
    color = mul(color, coefs[coefsIndex]);
#endif

    // HDR
#if defined(HDR)
    // BT2020 -> BT709
    color.rgb = pow(max(0.0, color.rgb), 2.4f);
    color.rgb = max(0.0, mul(color, bt2020tobt709color).rgb);
    color.rgb = pow(color.rgb, 1.0f / 2.2f);

    if (hdrmethod == Aces)
    {
        color.rgb = inversePQ(color.rgb);
        color.rgb *= (10000.0f / g_luminance) * (2.0f / g_toneP1);
        color.rgb = aces(color.rgb);
        color.rgb *= (1.24f / g_toneP1);
        color.rgb = pow(color.rgb, 0.27f);
    }
    else if (hdrmethod == Hable)
    {
        color.rgb = inversePQ(color.rgb);
        color.rgb *= g_toneP1;
        color.rgb = hable(color.rgb * g_toneP2) / hable(g_toneP2);
        color.rgb = pow(color.rgb, 1.0f / 2.2f);
    }
    else if (hdrmethod == Reinhard)
    {
        float luma = dot(color.rgb, bt709coefs);
        color.rgb *= reinhard(luma) / luma;
    }
    #if defined(HLG)
    // HLG
    color.rgb = inverseHLG(color.rgb);
    float3 ootf_2020 = float3(0.2627f, 0.6780f, 0.0593f);
    float ootf_ys = 2000.0f * dot(ootf_2020, color.rgb);
    color.rgb *= pow(ootf_ys, 0.2f);
    color.rgb = tranferPQ(color.rgb);
    #endif
#endif

    // Contrast / Brightness / Saturate / Hue
    color *= contrast * 2.0f;
    color += brightness - 0.5f;

    return color;
}
";

    const string VS = @"
cbuffer cBuf : register(b0)
{
    matrix mat;
}

struct VSInput
{
    float4 Position : POSITION;
    float2 Texture  : TEXCOORD;
};

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 Texture  : TEXCOORD;
};

PSInput main(VSInput vsi)
{
    PSInput psi;

    psi.Position = mul(vsi.Position, mat);
    psi.Texture = vsi.Texture;

    return psi;
}
";
}

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;
using FlyleafLib.MediaPlayer;
using FlyleafLib.MediaPlayer.Translation;
using FlyleafLib.MediaPlayer.Translation.Services;
using FlyleafLib.Plugins;
using static FlyleafLib.Utils;

namespace FlyleafLib;

/// <summary>
/// Player's configuration
/// </summary>
public class Config : NotifyPropertyChanged
{
    static JsonSerializerOptions jsonOpts = new() { WriteIndented = true };

    public Config()
    {
        // Parse default plugin options to Config.Plugins (Creates instances until fix with statics in interfaces)
        foreach (var plugin in Engine.Plugins.Types.Values)
        {
            var tmpPlugin = PluginHandler.CreatePluginInstance(plugin);
            var defaultOptions = tmpPlugin.GetDefaultOptions();
            tmpPlugin.Dispose();

            if (defaultOptions == null || defaultOptions.Count == 0) continue;

            Plugins.Add(plugin.Name, new ObservableDictionary<string, string>());
            foreach (var opt in defaultOptions)
                Plugins[plugin.Name].Add(opt.Key, opt.Value);
        }
        // save default plugin options for later
        _PluginsDefault = Plugins;

        Player.config = this;
        Demuxer.config = this;
    }

    public Config(bool test) { }

    public Config Clone()
    {
        Config config = new()
        {
            Audio       = Audio.Clone(),
            Video       = Video.Clone(),
            Subtitles   = Subtitles.Clone(),
            Demuxer     = Demuxer.Clone(),
            Decoder     = Decoder.Clone(),
            Player      = Player.Clone()
        };

        config.Player.config = config;
        config.Demuxer.config = config;

        return config;
    }
    public static Config Load(string path, JsonSerializerOptions jsonOptions = null)
    {
        Config config       = JsonSerializer.Deserialize<Config>(File.ReadAllText(path), jsonOptions);
        config.Loaded       = true;
        config.LoadedPath   = path;

        if (config.Audio.FiltersEnabled && Engine.Config.FFmpegLoadProfile == LoadProfile.Main)
            config.Audio.FiltersEnabled = false;

        // TODO: L: refactor
        config.Player.config = config;
        config.Demuxer.config = config;

        config.Subtitles.SetChildren();

        // Restore the plugin options initialized by the constructor, as they are overwritten during deserialization.

        // Remove removed plugin options
        foreach (var plugin in config.Plugins)
        {
            // plugin deleted
            if (!config._PluginsDefault.ContainsKey(plugin.Key))
            {
                config.Plugins.Remove(plugin.Key);
                continue;
            }

            // plugin option deleted
            foreach (var opt in plugin.Value)
            {
                if (!config._PluginsDefault[plugin.Key].ContainsKey(opt.Key))
                {
                    config.Plugins[plugin.Key].Remove(opt.Key);
                }
            }
        }

        // Restore added plugin options
        foreach (var plugin in config._PluginsDefault)
        {
            // plugin added
            if (!config.Plugins.ContainsKey(plugin.Key))
            {
                config.Plugins[plugin.Key] = plugin.Value;
                continue;
            }

            // plugin option added
            foreach (var opt in plugin.Value)
            {
                if (!config.Plugins[plugin.Key].ContainsKey(opt.Key))
                {
                    config.Plugins[plugin.Key][opt.Key] = opt.Value;
                }
            }
        }

        config.UpdateDefault();

        return config;
    }
    public void Save(string path = null, JsonSerializerOptions jsonOptions = null)
    {
        if (path == null)
        {
            if (string.IsNullOrEmpty(LoadedPath))
                return;

            path = LoadedPath;
        }

        jsonOptions ??= jsonOpts;

        File.WriteAllText(path, JsonSerializer.Serialize(this, jsonOptions));
    }

    private void UpdateDefault()
    {
        bool parsed = System.Version.TryParse(Version, out var loadedVer);

        if (!parsed || loadedVer <= System.Version.Parse("0.2.1"))
        {
            // for FlyleafLib v3.8.3, Ensure extension_picky is set
            Demuxer.FormatOpt = DemuxerConfig.DefaultVideoFormatOpt();
            Demuxer.AudioFormatOpt = DemuxerConfig.DefaultVideoFormatOpt();
            Demuxer.SubtitlesFormatOpt = DemuxerConfig.DefaultVideoFormatOpt();

            // for subtitles search #91
            int ctrlFBindingIdx = Player.KeyBindings.Keys
                .FindIndex(k => k.Key == System.Windows.Input.Key.F &&
                             k.Ctrl && !k.Alt && !k.Shift);
            if (ctrlFBindingIdx != -1)
            {
                // remove existing binding
                Player.KeyBindings.Keys.RemoveAt(ctrlFBindingIdx);
            }
            // set CTRL+F to subtitles search
            Player.KeyBindings.Keys.Add(new KeyBinding { Ctrl = true, Key = System.Windows.Input.Key.F, IsKeyUp = true, Action = KeyBindingAction.Custom, ActionName = "ActivateSubsSearch" });

            // Toggle always on top
            int ctrlTBindingIdx = Player.KeyBindings.Keys
                .FindIndex(k => k.Key == System.Windows.Input.Key.T &&
                             k.Ctrl && !k.Alt && !k.Shift);
            if (ctrlTBindingIdx == -1)
            {
                Player.KeyBindings.Keys.Add(new KeyBinding { Ctrl = true, Key = System.Windows.Input.Key.T, IsKeyUp = true, Action = KeyBindingAction.Custom, ActionName = "ToggleAlwaysOnTop" });
            }

            // Set Ctrl+A for ToggleSubsAutoTextCopy (previous Alt+A)
            int ctrlABindingIdx = Player.KeyBindings.Keys
                .FindIndex(k => k.Key == System.Windows.Input.Key.A &&
                             k.Ctrl && !k.Alt && !k.Shift);
            if (ctrlABindingIdx == -1)
            {
                Player.KeyBindings.Keys.Add(new KeyBinding { Ctrl = true, Key = System.Windows.Input.Key.A, IsKeyUp = true, Action = KeyBindingAction.Custom, ActionName = "ToggleSubsAutoTextCopy" });
            }
        }
    }

    internal void SetPlayer(Player player)
    {
        Player.player   = player;
        Player.KeyBindings.SetPlayer(player);
        Demuxer.player  = player;
        Decoder.player  = player;
        Audio.player    = player;
        Video.player    = player;
        Subtitles.SetPlayer(player);
    }

    public string           Version     { get; set; }

    /// <summary>
    /// Whether configuration has been loaded from file
    /// </summary>
    [JsonIgnore]
    public bool             Loaded      { get; private set; }

    /// <summary>
    /// The path that this configuration has been loaded from
    /// </summary>
    [JsonIgnore]
    public string           LoadedPath  { get; private set; }

    public PlayerConfig     Player      { get; set; } = new PlayerConfig();
    public DemuxerConfig    Demuxer     { get; set; } = new DemuxerConfig();
    public DecoderConfig    Decoder     { get; set; } = new DecoderConfig();
    public VideoConfig      Video       { get; set; } = new VideoConfig();
    public AudioConfig      Audio       { get; set; } = new AudioConfig();
    public SubtitlesConfig  Subtitles   { get; set; } = new SubtitlesConfig();
    public DataConfig       Data        { get; set; } = new DataConfig();

    public Dictionary<string, ObservableDictionary<string, string>>
                            Plugins     { get; set; } = new();
    private
           Dictionary<string, ObservableDictionary<string, string>>
                            _PluginsDefault;
    public class PlayerConfig : NotifyPropertyChanged
    {
        public PlayerConfig Clone()
        {
            PlayerConfig player = (PlayerConfig) MemberwiseClone();
            player.player = null;
            player.config = null;
            player.KeyBindings = KeyBindings.Clone();
            return player;
        }

        internal Player player;
        internal Config config;

        /// <summary>
        /// It will automatically start playing after open or seek after ended
        /// </summary>
        public bool     AutoPlay                    { get; set; } = true;

        /// <summary>
        /// Required buffered duration ticks before playing
        /// </summary>
        public long     MinBufferDuration {
            get => _MinBufferDuration;
            set
            {
                if (!Set(ref _MinBufferDuration, value)) return;
                if (config != null && value > config.Demuxer.BufferDuration)
                    config.Demuxer.BufferDuration = value;
            }
        }
        long _MinBufferDuration = 500 * 10000;

        /// <summary>
        /// Key bindings configuration
        /// </summary>
        public KeysConfig
                        KeyBindings                 { get; set; } = new KeysConfig();

        /// <summary>
        /// Fps while the player is not playing
        /// </summary>
        public double   IdleFps                     { get; set; } = 60.0;

        /// <summary>
        /// Max Latency (ticks) forces playback (with speed x1+) to stay at the end of the live network stream (default: 0 - disabled)
        /// </summary>
        public long     MaxLatency {
            get => _MaxLatency;
            set
            {
                if (value < 0)
                    value = 0;

                if (!Set(ref _MaxLatency, value)) return;

                if (value == 0)
                {
                    if (player != null)
                        player.Speed = 1;

                    if (config != null)
                        config.Decoder.LowDelay = false;

                    return;
                }

                // Large max buffer so we ensure the actual latency distance
                if (config != null)
                {
                    if (config.Demuxer.BufferDuration < value * 2)
                        config.Demuxer.BufferDuration = value * 2;

                    config.Decoder.LowDelay = true;
                }

                // Small min buffer to avoid enabling latency speed directly
                if (_MinBufferDuration > value / 10)
                    MinBufferDuration = value / 10;
            }
        }
        long _MaxLatency = 0;

        /// <summary>
        /// Min Latency (ticks) prevents MaxLatency to go (with speed x1) less than this limit (default: 0 - as low as possible)
        /// </summary>
        public long     MinLatency                  { get => _MinLatency; set => Set(ref _MinLatency, value); }
        long _MinLatency = 0;

        /// <summary>
        /// Prevents frequent speed changes when MaxLatency is enabled (to avoid audio/video gaps and desyncs)
        /// </summary>
        public long     LatencySpeedChangeInterval  { get; set; } = TimeSpan.FromMilliseconds(700).Ticks;

        /// <summary>
        /// Folder to save recordings (when filename is not specified)
        /// </summary>
        public string   FolderRecordings            { get; set; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Recordings");

        /// <summary>
        /// Folder to save snapshots (when filename is not specified)
        /// </summary>

        public string   FolderSnapshots             { get; set; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Snapshots");

        /// <summary>
        /// Forces CurTime/SeekBackward/SeekForward to seek accurate on video
        /// </summary>
        public bool     SeekAccurate                { get => _SeekAccurate; set => Set(ref _SeekAccurate, value); }
        bool _SeekAccurate;

        /// <summary>
        /// Margin time to move back forward when doing an exact seek (ticks)
        /// </summary>
        public long     SeekAccurateFixMargin       { get; set => Set(ref field, value); } = TimeSpan.FromMilliseconds(0).Ticks;

        /// <summary>
        /// Margin time to move back forward when getting frame (ticks)
        /// </summary>
        public long     SeekGetFrameFixMargin       { get; set => Set(ref field, value); } = TimeSpan.FromMilliseconds(3000).Ticks;

        /// <summary>
        /// Snapshot encoding will be used (valid formats bmp, png, jpg/jpeg)
        /// </summary>
        public string   SnapshotFormat              { get ;set; } = "bmp";

        /// <summary>
        /// Whether to refresh statistics about bitrates/fps/drops etc.
        /// </summary>
        public bool     Stats                       { get => _Stats; set => Set(ref _Stats, value); }
        bool _Stats = false;

        /// <summary>
        /// Sets playback's thread priority
        /// </summary>
        public ThreadPriority
                        ThreadPriority              { get; set; } = ThreadPriority.AboveNormal;

        /// <summary>
        /// Refreshes CurTime in UI on every frame (can cause performance issues)
        /// </summary>
        public bool     UICurTimePerFrame           { get; set; } = false;

        /// <summary>
        /// The upper limit of the volume amplifier
        /// </summary>
        public int      VolumeMax                   { get => _VolumeMax; set { Set(ref _VolumeMax, value); if (player != null && player.Audio.masteringVoice != null) player.Audio.masteringVoice.Volume = value / 100f;  } }
        int _VolumeMax = 150;

        /// <summary>
        /// The default volume
        /// </summary>
        public int      VolumeDefault               { get; set => Set(ref field, value); } = 75;

        /// <summary>
        /// The purpose of the player
        /// </summary>
        public Usage    Usage                       { get; set; } = Usage.AVS;

        // Offsets
        public long     AudioDelayOffset            { get; set; } =  100 * 10000;
        public long     AudioDelayOffset2           { get; set; } = 1000 * 10000;
        public long     SubtitlesDelayOffset        { get; set; } =  100 * 10000;
        public long     SubtitlesDelayOffset2       { get; set; } = 1000 * 10000;
        public long     SeekOffset                  { get; set; } = 1 * (long)1000 * 10000;
        public long     SeekOffset2                 { get; set; } = 5 * (long)1000 * 10000;
        public long     SeekOffset3                 { get; set; } = 15 * (long)1000 * 10000;
        public long     SeekOffset4                 { get; set; } = 30 * (long)1000 * 10000;
        public bool     SeekOffsetAccurate          { get; set; } = true;
        public bool     SeekOffsetAccurate2         { get; set; } = false;
        public bool     SeekOffsetAccurate3         { get; set; } = false;
        public bool     SeekOffsetAccurate4         { get; set; } = false;
        public double   SpeedOffset                 { get; set; } = 0.10;
        public double   SpeedOffset2                { get; set; } = 0.25;
        public int      ZoomOffset                  { get => _ZoomOffset; set { if (Set(ref _ZoomOffset, value)) player?.ResetAll(); } }
        int _ZoomOffset = 10;

        public int      VolumeOffset                { get; set; } = 5;
    }
    public class DemuxerConfig : NotifyPropertyChanged
    {
        public DemuxerConfig Clone()
        {
            DemuxerConfig demuxer = (DemuxerConfig) MemberwiseClone();

            demuxer.FormatOpt       = new Dictionary<string, string>();
            demuxer.AudioFormatOpt  = new Dictionary<string, string>();
            demuxer.SubtitlesFormatOpt = new Dictionary<string, string>();

            foreach (var kv in FormatOpt) demuxer.FormatOpt.Add(kv.Key, kv.Value);
            foreach (var kv in AudioFormatOpt) demuxer.AudioFormatOpt.Add(kv.Key, kv.Value);
            foreach (var kv in SubtitlesFormatOpt) demuxer.SubtitlesFormatOpt.Add(kv.Key, kv.Value);

            demuxer.player = null;
            demuxer.config = null;

            return demuxer;
        }

        internal Player player;
        internal Config config;

        /// <summary>
        /// Whethere to allow avformat_find_stream_info during open (avoiding this can open the input faster but it could cause other issues)
        /// </summary>
        public bool             AllowFindStreamInfo { get; set; } = true;

        /// <summary>
        /// Whether to enable demuxer's custom interrupt callback (for timeouts and interrupts)
        /// </summary>
        public bool             AllowInterrupts     { get; set; } = true;

        /// <summary>
        /// Whether to allow interrupts during av_read_frame
        /// </summary>
        public bool             AllowReadInterrupts { get; set; } = true;

        /// <summary>
        /// Whether to allow timeouts checks within the interrupts callback
        /// </summary>
        public bool             AllowTimeouts       { get; set; } = true;

        /// <summary>
        /// List of FFmpeg formats to be excluded from interrupts
        /// </summary>
        public List<string>     ExcludeInterruptFmts{ get; set; } = new List<string>() { "rtsp" };

        /// <summary>
        /// Maximum allowed duration ticks for buffering
        /// </summary>
        public long             BufferDuration      {
            get => _BufferDuration;
            set
            {
                if (!Set(ref _BufferDuration, value)) return;
                if (config != null && value < config.Player.MinBufferDuration)
                   config.Player.MinBufferDuration = value;
            }
        }
        long _BufferDuration = 30 * (long)1000 * 10000;

        /// <summary>
        /// Maximuim allowed packets for buffering (as an extra check along with BufferDuration)
        /// </summary>
        public long             BufferPackets   { get; set; }

        /// <summary>
        /// Maximuim allowed audio packets (when reached it will drop the extra packets and will fire the AudioLimit event)
        /// </summary>
        public long             MaxAudioPackets { get; set; }

        /// <summary>
        /// Maximum allowed errors before stopping
        /// </summary>
        public int              MaxErrors       { get; set; } = 30;

        /// <summary>
        /// Custom IO Stream buffer size (in bytes) for the AVIO Context
        /// </summary>
        public int              IOStreamBufferSize
                                                { get; set; } = 0x200000;

        /// <summary>
        /// avformat_close_input timeout (ticks) for protocols that support interrupts
        /// </summary>
        public long             CloseTimeout    { get => closeTimeout; set { closeTimeout = value; closeTimeoutMs = value / 10000; } }
        private long closeTimeout = 1 * 1000 * 10000;
        internal long closeTimeoutMs = 1 * 1000;

        /// <summary>
        /// avformat_open_input + avformat_find_stream_info timeout (ticks) for protocols that support interrupts (should be related to probesize/analyzeduration)
        /// </summary>
        public long             OpenTimeout     { get => openTimeout; set { openTimeout = value; openTimeoutMs = value / 10000; } }
        private long openTimeout = 5 * 60 * (long)1000 * 10000;
        internal long openTimeoutMs = 5 * 60 * 1000;

        /// <summary>
        /// av_read_frame timeout (ticks) for protocols that support interrupts
        /// </summary>
        public long             ReadTimeout     { get => readTimeout; set { readTimeout = value; readTimeoutMs = value / 10000; } }
        private long readTimeout = 10 * 1000 * 10000;
        internal long readTimeoutMs = 10 * 1000;

        /// <summary>
        /// av_read_frame timeout (ticks) for protocols that support interrupts (for Live streams)
        /// </summary>
        public long             ReadLiveTimeout { get => readLiveTimeout; set { readLiveTimeout = value; readLiveTimeoutMs = value / 10000; } }
        private long readLiveTimeout = 20 * 1000 * 10000;
        internal long readLiveTimeoutMs = 20 * 1000;

        /// <summary>
        /// av_seek_frame timeout (ticks) for protocols that support interrupts
        /// </summary>
        public long             SeekTimeout     { get => seekTimeout; set { seekTimeout = value; seekTimeoutMs = value / 10000; } }
        private long seekTimeout = 8 * 1000 * 10000;
        internal long seekTimeoutMs = 8 * 1000;

        /// <summary>
        /// Forces Input Format
        /// </summary>
        public string           ForceFormat     { get; set; }

        /// <summary>
        /// Forces FPS for NoTimestamp formats (such as h264/hevc)
        /// </summary>
        public double           ForceFPS        { get; set; }

        /// <summary>
        /// FFmpeg's format flags for demuxer (see https://ffmpeg.org/doxygen/trunk/avformat_8h.html)
        /// eg. FormatFlags |= 0x40; // For AVFMT_FLAG_NOBUFFER
        /// </summary>
        public DemuxerFlags     FormatFlags     { get; set; } = DemuxerFlags.DiscardCorrupt;// FFmpeg.AutoGen.ffmpeg.AVFMT_FLAG_DISCARD_CORRUPT;

        /// <summary>
        /// Certain muxers and demuxers do nesting (they open one or more additional internal format contexts). This will pass the FormatOpt and HTTPQuery params to the underlying contexts)
        /// </summary>
        public bool             FormatOptToUnderlying
                                                { get; set; }

        /// <summary>
        /// Passes original's Url HTTP Query String parameters to underlying
        /// </summary>
        public bool             DefaultHTTPQueryToUnderlying
                                                { get; set; } = true;

        /// <summary>
        /// HTTP Query String parameters to pass to underlying
        /// </summary>
        public Dictionary<string, string>
                                ExtraHTTPQueryParamsToUnderlying
                                                { get; set; } = new();

        /// <summary>
        /// FFmpeg's format options for demuxer
        /// </summary>
        public Dictionary<string, string>
                                FormatOpt       { get; set; } = DefaultVideoFormatOpt();
        public Dictionary<string, string>
                                AudioFormatOpt  { get; set; } = DefaultVideoFormatOpt();

        public Dictionary<string, string>
                                SubtitlesFormatOpt  { get; set; } = DefaultVideoFormatOpt();

        public static Dictionary<string, string> DefaultVideoFormatOpt()
        {
            // TODO: Those should be set based on the demuxer format/protocol (to avoid false warnings about invalid options and best fit for the input, eg. live stream)

            Dictionary<string, string> defaults = new()
            {
                // General
                { "probesize",          (50 * (long)1024 * 1024).ToString() },      // (Bytes) Default 5MB | Higher for weird formats (such as .ts?) and 4K/Hevc
                { "analyzeduration",    (10 * (long)1000 * 1000).ToString() },      // (Microseconds) Default 5 seconds | Higher for network streams

                // HTTP
                { "reconnect",          "1" },                                       // auto reconnect after disconnect before EOF
                { "reconnect_streamed", "1" },                                       // auto reconnect streamed / non seekable streams (this can cause issues with HLS ts segments - disable this or http_persistent)
                { "reconnect_delay_max","7" },                                       // max reconnect delay in seconds after which to give up
                { "user_agent",         "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" },

                { "extension_picky",    "0" },                                       // Added in ffmpeg v7.1.1 and causes issues when enabled with allowed extentions #577

                // HLS
                { "http_persistent",    "0" },                                       // Disables keep alive for HLS - mainly when use reconnect_streamed and non-live HLS streams

                // RTSP
                { "rtsp_transport",     "tcp" },                                     // Seems UDP causing issues
            };

            //defaults.Add("live_start_index",   "-1");
            //defaults.Add("timeout",           (2 * (long)1000 * 1000).ToString());      // (Bytes) Default 5MB | Higher for weird formats (such as .ts?)
            //defaults.Add("rw_timeout",     (2 * (long)1000 * 1000).ToString());      // (Microseconds) Default 5 seconds | Higher for network streams

            return defaults;
        }

        public Dictionary<string, string> GetFormatOptPtr(MediaType type)
            => type == MediaType.Video ? FormatOpt : type == MediaType.Audio ? AudioFormatOpt : SubtitlesFormatOpt;
    }
    public class DecoderConfig : NotifyPropertyChanged
    {
        internal Player player;

        public DecoderConfig Clone()
        {
            DecoderConfig decoder = (DecoderConfig) MemberwiseClone();
            decoder.player = null;

            return decoder;
        }

        /// <summary>
        /// Threads that will be used from the decoder
        /// </summary>
        public int              VideoThreads    { get; set; } = Environment.ProcessorCount;

        /// <summary>
        /// Maximum video frames to be decoded and processed for rendering
        /// </summary>
        public int              MaxVideoFrames  { get => _MaxVideoFrames; set { if (Set(ref _MaxVideoFrames, value)) { player?.RefreshMaxVideoFrames(); } } }
        int _MaxVideoFrames = 4;

        /// <summary>
        /// Maximum audio frames to be decoded and processed for playback
        /// </summary>
        public int              MaxAudioFrames  { get; set; } = 10;

        /// <summary>
        /// Maximum subtitle frames to be decoded
        /// </summary>
        public int              MaxSubsFrames   { get; set; } = 1;

        /// <summary>
        /// Maximum data frames to be decoded
        /// </summary>
        public int              MaxDataFrames   { get; set; } = 100;

        /// <summary>
        /// Maximum allowed errors before stopping
        /// </summary>
        public int              MaxErrors       { get; set; } = 200;

        /// <summary>
        /// Whether or not to use decoder's textures directly as shader resources
        /// (TBR: Better performance but might need to be disabled while video input has padding or not supported by older Direct3D versions)
        /// </summary>
        public ZeroCopy         ZeroCopy        { get => _ZeroCopy; set { if (SetUI(ref _ZeroCopy, value) && player != null && player.Video.isOpened) player.VideoDecoder?.RecalculateZeroCopy(); } }
        ZeroCopy _ZeroCopy = ZeroCopy.Auto;

        /// <summary>
        /// Allows video accceleration even in codec's profile mismatch
        /// </summary>
        public bool             AllowProfileMismatch
                                                { get => _AllowProfileMismatch; set => SetUI(ref _AllowProfileMismatch, value); }
        bool _AllowProfileMismatch;

        /// <summary>
        /// Allows corrupted frames (Parses AV_CODEC_FLAG_OUTPUT_CORRUPT to AVCodecContext)
        /// </summary>
        public bool             ShowCorrupted   { get => _ShowCorrupted; set => SetUI(ref _ShowCorrupted, value); }
        bool _ShowCorrupted;

        /// <summary>
        /// Forces low delay (Parses AV_CODEC_FLAG_LOW_DELAY to AVCodecContext) (auto-enabled with MaxLatency)
        /// </summary>
        public bool             LowDelay        { get => _LowDelay; set => SetUI(ref _LowDelay, value); }
        bool _LowDelay;

        public Dictionary<string, string>
                                AudioCodecOpt       { get; set; } = new();
        public Dictionary<string, string>
                                VideoCodecOpt       { get; set; } = new();
        public Dictionary<string, string>
                                SubtitlesCodecOpt   { get; set; } = new();

        public Dictionary<string, string> GetCodecOptPtr(MediaType type)
            => type == MediaType.Video ? VideoCodecOpt : type == MediaType.Audio ? AudioCodecOpt : SubtitlesCodecOpt;
    }
    public class VideoConfig : NotifyPropertyChanged
    {
        public VideoConfig Clone()
        {
            VideoConfig video = (VideoConfig) MemberwiseClone();
            video.player = null;

            return video;
        }

        internal Player player;

        /// <summary>
        /// <para>Forces a specific GPU Adapter to be used by the renderer</para>
        /// <para>GPUAdapter must match with the description of the adapter eg. rx 580 (available adapters can be found in Engine.Video.GPUAdapters)</para>
        /// </summary>
        public string           GPUAdapter                  { get; set; }

        /// <summary>
        /// Video aspect ratio
        /// </summary>
        public AspectRatio      AspectRatio                 { get => _AspectRatio;  set { if (Set(ref _AspectRatio, value) && player != null && player.renderer != null && !player.renderer.SCDisposed) lock(player.renderer.lockDevice) {  player.renderer.SetViewport(); if (player.renderer.child != null) player.renderer.child.SetViewport(); } } }
        AspectRatio    _AspectRatio = AspectRatio.Keep;

        /// <summary>
        /// Custom aspect ratio (AspectRatio must be set to Custom to have an effect)
        /// </summary>
        public AspectRatio      CustomAspectRatio           { get => _CustomAspectRatio;  set { if (Set(ref _CustomAspectRatio, value) && AspectRatio == AspectRatio.Custom) { _AspectRatio = AspectRatio.Fill; AspectRatio = AspectRatio.Custom; } } }
        AspectRatio    _CustomAspectRatio = new(16, 9);

        /// <summary>
        /// Background color of the player's control
        /// </summary>
        public System.Windows.Media.Color
                                BackgroundColor             { get => VorticeToWPFColor(_BackgroundColor);  set { Set(ref _BackgroundColor, WPFToVorticeColor(value)); player?.renderer?.UpdateBackgroundColor(); } }
        internal Vortice.Mathematics.Color _BackgroundColor = (Vortice.Mathematics.Color)Vortice.Mathematics.Colors.Black;

        /// <summary>
        /// Clears the screen on stop/close/open
        /// </summary>
        public bool             ClearScreen                 { get; set; } = true;

        /// <summary>
        /// Whether video should be allowed
        /// </summary>
        public bool             Enabled                     { get => _Enabled;          set { if (Set(ref _Enabled, value)) if (value) player?.Video.Enable(); else player?.Video.Disable(); } }
        bool _Enabled = true;
        internal void SetEnabled(bool enabled)              => Set(ref _Enabled, enabled, true, nameof(Enabled));

        /// <summary>
        /// Used to limit the number of frames rendered, particularly at increased speed
        /// </summary>
        public double           MaxOutputFps                { get; set; } = 60;

        /// <summary>
        /// DXGI Maximum Frame Latency (1 - 16)
        /// </summary>
        public uint             MaxFrameLatency             { get; set; } = 1;

        /// <summary>
        /// The max resolution that the current system can achieve and will be used from the input/stream suggester plugins
        /// </summary>
        [JsonIgnore]
        public int              MaxVerticalResolutionAuto   { get; internal set; }

        /// <summary>
        /// Custom max vertical resolution that will be used from the input/stream suggester plugins
        /// </summary>
        public int              MaxVerticalResolutionCustom { get => _MaxVerticalResolutionCustom; set => Set(ref _MaxVerticalResolutionCustom, value); }
        int _MaxVerticalResolutionCustom;

        /// <summary>
        /// The max resolution that is currently used (based on Auto/Custom)
        /// </summary>
        [JsonIgnore]
        public int              MaxVerticalResolution       => MaxVerticalResolutionCustom == 0 ? (MaxVerticalResolutionAuto != 0 ? MaxVerticalResolutionAuto : 1080) : MaxVerticalResolutionCustom;

        /// <summary>
        /// In case of no hardware accelerated or post process accelerated pixel formats will use FFmpeg's SwsScale
        /// </summary>
        public bool             SwsHighQuality              { get; set; } = false;

        /// <summary>
        /// Forces SwsScale instead of FlyleafVP for non HW decoded frames
        /// </summary>
        public bool             SwsForce                    { get; set; } = false;

        /// <summary>
        /// Activates Direct3D video acceleration (decoding)
        /// </summary>
        public bool             VideoAcceleration           { get; set => Set(ref field, value); } = true;

        /// <summary>
        /// Whether to use embedded video processor with custom pixel shaders or D3D11<br/>
        /// (Currently D3D11 works only on video accelerated / hardware surfaces)<br/>
        /// * FLVP supports HDR to SDR, D3D11 does not<br/>
        /// * FLVP supports Pan Move/Zoom, D3D11 does not<br/>
        /// * D3D11 possible performs better with color conversion and filters, FLVP supports only brightness/contrast filters<br/>
        /// * D3D11 supports deinterlace (bob)
        /// </summary>
        public VideoProcessors  VideoProcessor              { get => _VideoProcessor; set { if (Set(ref _VideoProcessor, value)) player?.renderer?.UpdateVideoProcessor(); } }
        VideoProcessors _VideoProcessor = VideoProcessors.Auto;

        /// <summary>
        /// Whether Vsync should be enabled (0: Disabled, 1: Enabled)
        /// </summary>
        public uint             VSync                       { get; set; }

        /// <summary>
        /// Swap chain's present flags (mainly for waitable -None- or non-waitable -DoNotWait) (default: non-waitable)<br/>
        /// Non-waitable swap chain will reduce re-buffering and audio/video desyncs
        /// </summary>
        public Vortice.DXGI.PresentFlags
                                PresentFlags                { get; set; } = Vortice.DXGI.PresentFlags.DoNotWait;

        /// <summary>
        /// Enables the video processor to perform post process deinterlacing
        /// (D3D11 video processor should be enabled and support bob deinterlace method)
        /// </summary>
        public bool             Deinterlace                 { get=> _Deinterlace;   set { if (Set(ref _Deinterlace, value)) player?.renderer?.UpdateDeinterlace(); } }
        bool _Deinterlace;

        public bool             DeinterlaceBottomFirst      { get=> _DeinterlaceBottomFirst; set { if (Set(ref _DeinterlaceBottomFirst, value)) player?.renderer?.UpdateDeinterlace(); } }
        bool _DeinterlaceBottomFirst;

        /// <summary>
        /// The HDR to SDR method that will be used by the pixel shader
        /// </summary>
        public unsafe HDRtoSDRMethod
                                HDRtoSDRMethod              { get => _HDRtoSDRMethod; set { if (Set(ref _HDRtoSDRMethod, value) && player != null && player.VideoDecoder.VideoStream != null && player.VideoDecoder.VideoStream.ColorSpace == ColorSpace.BT2020) player.renderer.UpdateHDRtoSDR(); }}
        HDRtoSDRMethod _HDRtoSDRMethod = HDRtoSDRMethod.Hable;

        /// <summary>
        /// The HDR to SDR Tone float correnction (not used by Reinhard)
        /// </summary>
        public unsafe float     HDRtoSDRTone                { get => _HDRtoSDRTone; set { if (Set(ref _HDRtoSDRTone, value) && player != null && player.VideoDecoder.VideoStream != null && player.VideoDecoder.VideoStream.ColorSpace == ColorSpace.BT2020) player.renderer.UpdateHDRtoSDR(); } }
        float _HDRtoSDRTone = 1.4f;

        /// <summary>
        /// Whether the renderer will use 10-bit swap chaing or 8-bit output
        /// </summary>
        public bool             Swap10Bit                   { get; set; }

        /// <summary>
        /// The number of buffers to use for the renderer's swap chain
        /// </summary>
        public uint             SwapBuffers                 { get; set; } = 2;

        /// <summary>
        /// <para>
        /// Whether the renderer will use R8G8B8A8_UNorm instead of B8G8R8A8_UNorm format for the swap chain (experimental)<br/>
        /// (TBR: causes slightly different colors with D3D11VP)
        /// </para>
        /// </summary>
        public bool             SwapForceR8G8B8A8           { get; set; }

        public Dictionary<VideoFilters, VideoFilter> Filters {get ; set; } = DefaultFilters();

        public static Dictionary<VideoFilters, VideoFilter> DefaultFilters()
        {
            Dictionary<VideoFilters, VideoFilter> filters = new();

            var available = Enum.GetValues(typeof(VideoFilters));

            foreach(object filter in available)
                filters.Add((VideoFilters)filter, new VideoFilter((VideoFilters)filter));

            return filters;
        }
    }
    public class AudioConfig : NotifyPropertyChanged
    {
        public AudioConfig Clone()
        {
            AudioConfig audio = (AudioConfig) MemberwiseClone();
            audio.player = null;

            return audio;
        }

        internal Player player;

        /// <summary>
        /// Audio delay ticks (will be reseted to 0 for every new audio stream)
        /// </summary>
        public long             Delay               { get => _Delay;            set { if (player != null && !player.Audio.IsOpened) return;  if (Set(ref _Delay, value)) player?.ReSync(player.decoder.AudioStream); } }
        long _Delay;
        internal void SetDelay(long delay)          => Set(ref _Delay, delay, true, nameof(Delay));

        /// <summary>
        /// Whether audio should allowed
        /// </summary>
        public bool             Enabled             { get => _Enabled;          set { if (Set(ref _Enabled, value)) if (value) player?.Audio.Enable(); else player?.Audio.Disable(); } }
        bool _Enabled = true;
        internal void SetEnabled(bool enabled)      => Set(ref _Enabled, enabled, true, nameof(Enabled));

        /// <summary>
        /// Whether to process samples with Filters or SWR (experimental)<br/>
        /// 1. Requires FFmpeg avfilter lib<br/>
        /// 2. Currently SWR performs better if you dont need filters<br/>
        /// </summary>
        public bool             FiltersEnabled      { get => _FiltersEnabled; set { if (Set(ref _FiltersEnabled, value && Engine.Config.FFmpegLoadProfile != LoadProfile.Main)) player?.AudioDecoder.SetupFiltersOrSwr(); } }
        bool _FiltersEnabled = false;

        /// <summary>
        /// List of filters for post processing the audio samples (experimental)<br/>
        /// (Requires FiltersEnabled)
        /// </summary>
        public List<Filter>     Filters             { get; set; }

        /// <summary>
        /// Audio languages preference by priority
        /// </summary>
        public List<Language>   Languages
        {
            get
            {
                field ??= GetSystemLanguages();
                return field;
            }
            set => Set(ref field, value);
        }

    }

    public class SubConfig : NotifyPropertyChanged
    {
        internal Player player;

        public SubConfig()
        {
        }

        public SubConfig(int subIndex)
        {
            SubIndex = subIndex;
        }

        [JsonIgnore]
        public int SubIndex { get; set => Set(ref field, value); }

        [JsonIgnore]
        public bool EnabledTranslated
        {
            get;
            set
            {
                if (Set(ref field, value))
                {
                    if (player == null)
                    {
                        return;
                    }

                    // Clear once to update the subtitle being displayed.
                    player.sFramesPrev[SubIndex] = null;
                    player.SubtitleClear(SubIndex);

                    // Switching the display while leaving the translated text itself
                    foreach (SubtitleData sub in player.SubtitlesManager[SubIndex].Subs)
                    {
                        sub.EnabledTranslated = field;
                    }

                    // Update text in sidebar
                    player.SubtitlesManager[SubIndex].Refresh();
                }
            }
        } = false;

        /// <summary>
        /// Subtitle delay ticks (will be reset to 0 for every new subtitle stream)
        /// </summary>
        public long Delay
        {
            get => _delay;
            set
            {
                if (player == null || !player.Subtitles[SubIndex].Enabled)
                {
                    return;
                }
                if (Set(ref _delay, value))
                {
                    player.SubtitlesManager[SubIndex].SetCurrentTime(new TimeSpan(player.CurTime));
                    player.ReSync(player.decoder.SubtitlesStreams[SubIndex]);
                }
            }
        }
        private long _delay;

        internal void SetDelay(long delay) => Set(ref _delay, delay, true, nameof(Delay));

        /// <summary>
        /// Whether subtitle should be visible
        /// TODO: L: should move to AppConfig?
        /// </summary>
        [JsonIgnore]
        public bool Visible { get; set => Set(ref field, value); } = true;

        /// <summary>
        /// OCR Engine Type
        /// </summary>
        public SubOCREngineType OCREngine { get; set => Set(ref field, value); } = SubOCREngineType.Tesseract;
    }

    public class SubtitlesConfig : NotifyPropertyChanged
    {
        public SubConfig[] SubConfigs { get; set; }

        public SubConfig this[int subIndex] => SubConfigs[subIndex];

        public SubtitlesConfig()
        {
            int subNum = 2;
            SubConfigs = new SubConfig[subNum];
            for (int i = 0; i < subNum; i++)
            {
                SubConfigs[i] = new SubConfig(i);
            }
        }

        internal void SetChildren()
        {
            for (int i = 0; i < SubConfigs.Length; i++)
            {
                SubConfigs[i].SubIndex = i;
            }
        }

        public SubtitlesConfig Clone()
        {
            SubtitlesConfig subs = new();
            subs = (SubtitlesConfig) MemberwiseClone();

            subs.Languages = new List<Language>();
            if (Languages != null) foreach(var lang in Languages) subs.Languages.Add(lang);

            subs.player = null;

            return subs;
        }

        internal Player player;
        internal void SetPlayer(Player player)
        {
            this.player = player;

            foreach (SubConfig conf in SubConfigs)
            {
                conf.player = player;
            }
        }

        /// <summary>
        /// Whether subtitles should be allowed
        /// </summary>
        public bool             Enabled             { get => _Enabled; set { if(Set(ref _Enabled, value)) if (value) player?.Subtitles.Enable(); else player?.Subtitles.Disable(); } }
        bool _Enabled = true;
        internal void SetEnabled(bool enabled)      => Set(ref _Enabled, enabled, true, nameof(Enabled));

        /// <summary>
        /// Max number of subtitles (currently not configurable)
        /// </summary>
        public int Max { get; set => Set(ref field, value); } = 2;

        /// <summary>
        /// Whether to cache internal bitmap subtitles on memory
        /// Memory usage is larger since all bitmap are read on memory, but has the following advantages
        /// 1. Internal bitmap subtitles can be displayed in the sidebar
        /// 2. Can display bitmap subtitles during playback when seeking (mpv: can, VLC: cannot)
        /// </summary>
        public bool EnabledCached { get; set => Set(ref field, value); } = true;

        public bool OpenAutomaticSubs { get; set => Set(ref field, value); }

        /// <summary>
        /// Subtitle languages preference by priority
        /// </summary>
        public List<Language> Languages
        {
            get
            {
                field ??= GetSystemLanguages();
                return field;
            }
            set => Set(ref field, value);
        }

        /// <summary>
        /// Whether to use automatic language detection
        /// </summary>
        public bool LanguageAutoDetect { get; set => Set(ref field, value); } = true;

        /// <summary>
        /// Language to be used when source language was unknown (primary)
        /// </summary>
        public Language LanguageFallbackPrimary
        {
            get
            {
                field ??= Languages.FirstOrDefault();
                return field;
            }
            set => Set(ref field, value);
        }

        /// <summary>
        /// Language to be used when source language was unknown (secondary)
        /// </summary>
        public Language LanguageFallbackSecondary
        {
            get
            {
                if (LanguageFallbackSecondarySame)
                {
                    return LanguageFallbackPrimary;
                }

                field ??= LanguageFallbackPrimary;

                return field;
            }
            set => Set(ref field, value);
        }

        /// <summary>
        /// Whether to use LanguageFallbackPrimary for secondary subtitles
        /// </summary>
        public bool LanguageFallbackSecondarySame
        {
            get;
            set
            {
                if (Set(ref field, value))
                {
                    Raise(nameof(LanguageFallbackSecondary));
                }
            }
        } = true;

        /// <summary>
        /// Whether to use local search plugins (see also <see cref="SearchLocalOnInputType"/>)
        /// </summary>
        public bool             SearchLocal         { get => _SearchLocal; set => Set(ref _SearchLocal, value); }
        bool _SearchLocal = false;

        public const string DefaultSearchLocalPaths = "subs; subtitles";

        public string           SearchLocalPaths
        {
            get;
            set
            {
                if (Set(ref field, value))
                {
                    CmdResetSearchLocalPaths.OnCanExecuteChanged();
                }
            }
        } = DefaultSearchLocalPaths;

        [JsonIgnore]
        public RelayCommand CmdResetSearchLocalPaths => field ??= new((_) =>
        {
            SearchLocalPaths = DefaultSearchLocalPaths;
        }, (_) => SearchLocalPaths != DefaultSearchLocalPaths);

        /// <summary>
        /// Allowed input types to be searched locally for subtitles (empty list allows all types)
        /// </summary>
        public List<InputType>  SearchLocalOnInputType
                                                    { get; set; } = new List<InputType>() { InputType.File, InputType.UNC, InputType.Torrent };

        /// <summary>
        /// Whether to use online search plugins (see also <see cref="SearchOnlineOnInputType"/>)
        /// </summary>
        public bool             SearchOnline        { get => _SearchOnline; set { Set(ref _SearchOnline, value); if (player != null && player.Video.isOpened) Task.Run(() => { if (player != null && player.Video.isOpened) player.decoder.SearchOnlineSubtitles(); }); } }
        bool _SearchOnline = false;

        /// <summary>
        /// Allowed input types to be searched online for subtitles (empty list allows all types)
        /// </summary>
        public List<InputType>  SearchOnlineOnInputType
                                                    { get; set; } = new List<InputType>() { InputType.File, InputType.Torrent };

        /// <summary>
        /// Subtitles parser (can be used for custom parsing)
        /// </summary>
        [JsonIgnore]
        public Action<SubtitlesFrame>
                                Parser              { get; set; } = ParseSubtitles.Parse;

        #region ASR
        /// <summary>
        /// ASR Engine Type (Currently only supports OpenAI Whisper)
        /// </summary>
        public SubASREngineType ASREngine { get; set => Set(ref field, value); } = SubASREngineType.WhisperCpp;

        /// <summary>
        /// ASR OpenAI Whisper common config
        /// </summary>
        public WhisperConfig WhisperConfig { get; set => Set(ref field, value); } = new();

        /// <summary>
        /// ASR whisper.cpp config
        /// </summary>
        public WhisperCppConfig WhisperCppConfig { get; set => Set(ref field, value); } = new();

        /// <summary>
        /// ASR Faster-Whisper config
        /// </summary>
        public FasterWhisperConfig FasterWhisperConfig { get; set => Set(ref field, value); } = new();

        /// <summary>
        /// Chunk size (MB) when processing ASR with audio stream
        /// Increasing size will increase memory usage but may result in more natural subtitle breaks
        /// </summary>
        public int ASRChunkSizeMB
        {
            get;
            set
            {
                if (Set(ref field, value))
                {
                    Raise(nameof(ASRChunkSize));
                }
            }
        } = 20;

        [JsonIgnore]
        public long ASRChunkSize => ASRChunkSizeMB * 1024 * 1024;

        /// <summary>
        /// Chunk seconds when processing ASR with audio stream
        /// In the case of network streams, etc., the size is small and can be divided by specifying the number of seconds.
        /// </summary>
        public int ASRChunkSeconds { get; set => Set(ref field, value); } = 20;
        #endregion

        #region OCR
        /// <summary>
        /// OCR Tesseract Region Settings (key: iso6391, value: LangCode)
        /// </summary>
        public Dictionary<string, string> TesseractOcrRegions { get; set => Set(ref field, value); } = new();

        /// <summary>
        /// OCR Microsoft Region Settings (key: iso6391, value: LanguageTag (BCP-47)
        /// </summary>
        public Dictionary<string, string> MsOcrRegions { get; set => Set(ref field, value); } = new();
        #endregion

        #region Translation
        /// <summary>
        /// Language to be translated to
        /// </summary>
        public TargetLanguage TranslateTargetLanguage
        {
            get;
            set
            {
                if (Set(ref field, value))
                {
                    TranslateLanguage = Language.Get(value.ToISO6391());
                }
            }
        } = TargetLanguage.EnglishAmerican;

        [JsonIgnore]
        public Language TranslateLanguage { get; private set; }

        /// <summary>
        /// Translation Service Type
        /// </summary>
        public TranslateServiceType TranslateServiceType { get; set => Set(ref field, value); } = TranslateServiceType.GoogleV1;

        /// <summary>
        /// Translation Word Service Type
        /// </summary>
        public TranslateServiceType TranslateWordServiceType { get; set => Set(ref field, value); } = TranslateServiceType.GoogleV1;

        /// <summary>
        /// Translation Service Type Settings
        /// </summary>
        public Dictionary<TranslateServiceType, ITranslateSettings> TranslateServiceSettings { get; set => Set(ref field, value); } = new();

        /// <summary>
        /// Maximum count backward
        /// </summary>
        public int TranslateCountBackward { get; set => Set(ref field, value); } = 1;

        /// <summary>
        /// Maximum count forward
        /// </summary>
        public int TranslateCountForward { get; set => Set(ref field, value); } = 12;

        /// <summary>
        /// Number of concurrent requests to translation services
        /// </summary>
        public int TranslateMaxConcurrency {
            get;
            set
            {
                if (value <= 0)
                    return;

                Set(ref field, value);
            }
        } = 2;

        /// <summary>
        /// Chat-style LLM API config
        /// </summary>
        public TranslateChatConfig TranslateChatConfig { get; set => Set(ref field, value); } = new();
        #endregion
    }
    public class DataConfig : NotifyPropertyChanged
    {
        public DataConfig Clone()
        {
            DataConfig data = new();
            data = (DataConfig)MemberwiseClone();

            data.player = null;

            return data;
        }

        internal Player player;

        /// <summary>
        /// Whether data should be processed
        /// </summary>
        public bool             Enabled             { get => _Enabled; set { if (Set(ref _Enabled, value)) if (value) player?.Data.Enable(); else player?.Data.Disable(); } }
        bool _Enabled = false;
        internal void SetEnabled(bool enabled) => Set(ref _Enabled, enabled, true, nameof(Enabled));
    }
}

/// <summary>
/// Engine's configuration
/// </summary>
public class EngineConfig
{
    public string   Version                 { get; set; }

    /// <summary>
    /// It will not initiallize audio and will be disabled globally
    /// </summary>
    public bool     DisableAudio            { get; set; }

    /// <summary>
    /// <para>Required to register ffmpeg libraries. Make sure you provide x86 or x64 based on your project.</para>
    /// <para>:<path> for relative path from current folder or any below</para>
    /// <para><path> for absolute or relative path</para>
    /// </summary>
    public string   FFmpegPath              { get; set; } = "FFmpeg";

    /// <summary>
    /// <para>Can be used to choose which FFmpeg libs to load</para>
    /// All (Devices & Filters)<br/>
    /// Filters<br/>
    /// Main<br/>
    /// </summary>
    public LoadProfile
                    FFmpegLoadProfile       { get; set; } = LoadProfile.Filters; // change default to disable devices

    /// <summary>
    /// Whether to allow HLS live seeking (this can cause segmentation faults in case of incompatible ffmpeg version with library's custom structures)
    /// </summary>
    public bool     FFmpegHLSLiveSeek       { get; set; }

    /// <summary>
    /// Sets FFmpeg logger's level
    /// </summary>
    public Flyleaf.FFmpeg.LogLevel
                    FFmpegLogLevel          { get => _FFmpegLogLevel; set { _FFmpegLogLevel = value; if (Engine.IsLoaded) FFmpegEngine.SetLogLevel(); } }
    Flyleaf.FFmpeg.LogLevel _FFmpegLogLevel = Flyleaf.FFmpeg.LogLevel.Quiet;

    /// <summary>
    /// Whether configuration has been loaded from file
    /// </summary>
    [JsonIgnore]
    public bool     Loaded                  { get; private set; }

    /// <summary>
    /// The path that this configuration has been loaded from
    /// </summary>
    [JsonIgnore]
    public string   LoadedPath              { get; private set; }

    /// <summary>
    /// <para>Sets loggers output</para>
    /// <para>:debug -> System.Diagnostics.Debug</para>
    /// <para>:console -> System.Console</para>
    /// <para><path> -> Absolute or relative file path</para>
    /// </summary>
    public string   LogOutput               { get => _LogOutput; set { _LogOutput = value; if (Engine.IsLoaded) Logger.SetOutput(); } }
    string _LogOutput = "";

    /// <summary>
    /// Sets logger's level
    /// </summary>
    public LogLevel LogLevel                { get; set; } = LogLevel.Quiet;

    /// <summary>
    /// When the output is file it will append instead of overwriting
    /// </summary>
    public bool     LogAppend               { get; set; }

    /// <summary>
    /// Lines to cache before writing them to file
    /// </summary>
    public int      LogCachedLines          { get; set; } = 20;

    /// <summary>
    /// Sets the logger's datetime string format
    /// </summary>
    public string   LogDateTimeFormat       { get; set; } = "HH.mm.ss.fff";

    /// <summary>
    /// <para>Required to register plugins. Make sure you provide x86 or x64 based on your project and same .NET framework.</para>
    /// <para>:<path> for relative path from current folder or any below</para>
    /// <para><path> for absolute or relative path</para>
    /// </summary>
    public string   PluginsPath             { get; set; } = "Plugins";

    /// <summary>
    /// Updates Player.CurTime when the second changes otherwise on every UIRefreshInterval
    /// </summary>
    public bool     UICurTimePerSecond      { get; set; } = true;

    /// <summary>
    /// <para>Activates Master Thread to monitor all the players and perform the required updates</para>
    /// <para>Required for Activity Mode, Stats & Buffered Duration on Pause</para>
    /// </summary>
    public bool     UIRefresh               { get => _UIRefresh; set { _UIRefresh = value; if (value && Engine.IsLoaded) Engine.StartThread(); } }
    static bool _UIRefresh;

    /// <summary>
    /// <para>How often should update the UI in ms (low values can cause performance issues)</para>
    /// <para>Should UIRefreshInterval < 1000ms and 1000 % UIRefreshInterval == 0 for accurate per second stats</para>
    /// </summary>
    public int      UIRefreshInterval       { get; set; } = 250;

    /// <summary>
    /// Loads engine's configuration
    /// </summary>
    /// <param name="path">Absolute or relative path to load the configuraiton</param>
    /// <param name="jsonOptions">JSON serializer options</param>
    // <returns></returns>
    public static EngineConfig Load(string path, JsonSerializerOptions jsonOptions = null)
    {
        EngineConfig config = JsonSerializer.Deserialize<EngineConfig>(File.ReadAllText(path), jsonOptions);
        config.Loaded       = true;
        config.LoadedPath   = path;

        return config;
    }

    /// <summary>
    /// Saves engine's current configuration
    /// </summary>
    /// <param name="path">Absolute or relative path to save the configuration</param>
    /// <param name="jsonOptions">JSON serializer options</param>
    public void Save(string path = null, JsonSerializerOptions jsonOptions = null)
    {
        if (path == null)
        {
            if (string.IsNullOrEmpty(LoadedPath))
                return;

            path = LoadedPath;
        }

        jsonOptions ??= new JsonSerializerOptions { WriteIndented = true };

        File.WriteAllText(path, JsonSerializer.Serialize(this, jsonOptions));
    }
}

using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Media;
using System.Text;
using System.Windows;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.ViewModels;

public class ErrorDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public ErrorDialogVM(FlyleafManager fl)
    {
        FL = fl;
    }

    public string Message { get; set => Set(ref field, value); } = "";

    public Exception? Exception
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(HasException));
                ExceptionDetail = value == null ? "" : GetExceptionWithAllData(value);
            }
        }
    }
    public bool HasException => Exception != null;

    public string ExceptionDetail { get; set => Set(ref field, value); } = "";

    public bool IsUnknown
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(ErrorTitle));
            }
        }
    }

    public string ErrorType
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(ErrorTitle));
            }
        }
    } = "";

    public string ErrorTitle => IsUnknown ? $"{ErrorType} Unknown Error" : $"{ErrorType} Error";

    [field: AllowNull, MaybeNull]
    public DelegateCommand CmdCopyMessage => field ??= new(() =>
    {
        string text = $"""
                       [{ErrorTitle}]
                       {Message}
                       """;

        if (Exception != null)
        {
            text += $"""

                    ```
                    {ExceptionDetail}
                    ```
                    """;
        }

        text += $"""

                 Version: {App.Version}, CommitHash: {App.CommitHash}
                 OS Architecture: {App.OSArchitecture}, Process Architecture: {App.ProcessArchitecture}
                 """;
        Clipboard.SetText(text);
    });

    [field: AllowNull, MaybeNull]
    public DelegateCommand CmdCloseDialog => field ??= new(() =>
    {
        RequestClose.Invoke(ButtonResult.OK);
    });

    private static string GetExceptionWithAllData(Exception ex)
    {
        string exceptionInfo = ex.ToString();

        // Collect all Exception.Data to dictionary
        Dictionary<object, object> allData = new();
        CollectExceptionData(ex, allData);

        if (allData.Count == 0)
        {
            // not found Exception.Data
            return exceptionInfo;
        }

        // found Exception.Data
        StringBuilder sb = new();
        sb.Append(exceptionInfo);

        sb.AppendLine();
        sb.AppendLine();
        sb.AppendLine("---------------------- All Exception Data ----------------------");
        foreach (var (i, entry) in allData.Index())
        {
            sb.AppendLine($"  [{entry.Key}]: {entry.Value}");
            if (i != allData.Count - 1)
            {
                sb.AppendLine();
            }
        }

        return sb.ToString();
    }

    private static void CollectExceptionData(Exception? ex, Dictionary<object, object> allData)
    {
        if (ex == null) return;

        foreach (DictionaryEntry entry in ex.Data)
        {
            if (entry.Value != null)
            {
                allData.TryAdd(entry.Key, entry.Value);
            }
        }

        if (ex.InnerException != null)
        {
            CollectExceptionData(ex.InnerException, allData);
        }

        if (ex is AggregateException aggregateEx)
        {
            foreach (var innerEx in aggregateEx.InnerExceptions)
            {
                CollectExceptionData(innerEx, allData);
            }
        }
    }

    #region IDialogAware
    public string Title => "Error Occured";
    public double WindowWidth { get; set => Set(ref field, value); } = 450;
    public double WindowHeight { get; set => Set(ref field, value); } = 250;

    public bool CanCloseDialog() => true;

    public void OnDialogClosed()
    {
        FL.Player.Activity.Timeout = _prevTimeout;
        FL.Player.Activity.IsEnabled = true;
    }

    private int _prevTimeout;

    public void OnDialogOpened(IDialogParameters parameters)
    {
        _prevTimeout = FL.Player.Activity.Timeout;
        FL.Player.Activity.Timeout = 0;
        FL.Player.Activity.IsEnabled = false;

        switch (parameters.GetValue<string>("type"))
        {
            case "known":
                Message = parameters.GetValue<string>("message");
                ErrorType = parameters.GetValue<string>("errorType");
                IsUnknown = false;

                break;
            case "unknown":
                Message = parameters.GetValue<string>("message");
                ErrorType = parameters.GetValue<string>("errorType");
                IsUnknown = true;

                if (parameters.ContainsKey("exception"))
                {
                    Exception = parameters.GetValue<Exception>("exception");
                }

                WindowHeight += 100;
                WindowWidth += 20;

                // Play alert sound
                SystemSounds.Hand.Play();

                break;
        }
    }

    public DialogCloseListener RequestClose { get; }
    #endregion IDialogAware
}

using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;

using Vortice;
using Vortice.DXGI;
using Vortice.Direct3D11;
using Vortice.Mathematics;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaStream;

using ID3D11Device = Vortice.Direct3D11.ID3D11Device;

namespace FlyleafLib.MediaFramework.MediaRenderer;

/* TODO
 * 1) Attach on every frame video output configuration so we will not have to worry for video codec change etc.
 *      this will fix also dynamic video stream change
 *      we might have issue with bufRef / ffmpeg texture array on zero copy
 *
 * 2) Use different context/video processor for off rendering so we dont have to reset pixel shaders/viewports etc (review also rtvs for extractor)
 *
 * 3) Add Crop (Left/Right/Top/Bottom) -on Source- support per pixels (easy implemantation with D3D11VP, FlyleafVP requires more research)
 *
 * 4) Improve A/V Sync
 *
 *  a. vsync / vblack
 *  b. Present can cause a delay (based on device load), consider using more buffers for high frame rates that could minimize the delay
 *  c. display refresh rate vs input rate, consider using max latency > 1 ?
 *  d. frame drops
 *
 *      swapChain.GetFrameStatistics(out var stats);
 *      swapChain.LastPresentCount - stats.PresentCount;
 */

public partial class Renderer : NotifyPropertyChanged, IDisposable
{
    public Config           Config          { get; private set;}
    public int              ControlWidth    { get; private set; }
    public int              ControlHeight   { get; private set; }
    internal IntPtr         ControlHandle;

    internal Action<IDXGISwapChain2>
                            SwapChainWinUIClbk;

    public ID3D11Device     Device          { get; private set; }
    public bool             D3D11VPFailed   => vc == null;
    public GPUAdapter       GPUAdapter      { get; private set; }
    public bool             Disposed        { get; private set; } = true;
    public bool             SCDisposed      { get; private set; } = true;
    public int              MaxOffScreenTextures
                                            { get; set; } = 20;
    public VideoDecoder     VideoDecoder    { get; internal set; }
    public VideoStream      VideoStream     => VideoDecoder.VideoStream;

    public Viewport         GetViewport     { get; private set; }
    public event EventHandler ViewportChanged;

    public CornerRadius     CornerRadius    { get => cornerRadius;  set { if (cornerRadius == value) return; cornerRadius = value; UpdateCornerRadius(); } }
    CornerRadius cornerRadius = new(0);
    CornerRadius zeroCornerRadius = new(0);

    public bool             IsHDR           { get => isHDR;         private set { SetUI(ref _IsHDR, value); isHDR = value; } }
    bool _IsHDR, isHDR;

    public int              SideXPixels     { get; private set; }
    public int              SideYPixels     { get; private set; }

    public int              PanXOffset      { get => panXOffset;    set => SetPanX(value); }
    int panXOffset;
    public void SetPanX(int panX, bool refresh = true)
    {
        lock(lockDevice)
        {
            panXOffset = panX;

            if (Disposed)
                return;

            SetViewport(refresh);
        }
    }

    public int              PanYOffset      { get => panYOffset;    set => SetPanY(value); }
    int panYOffset;
    public void SetPanY(int panY, bool refresh = true)
    {
        lock(lockDevice)
        {
            panYOffset = panY;

            if (Disposed)
                return;

            SetViewport(refresh);
        }
    }

    public uint             Rotation        { get => _RotationAngle;set { lock (lockDevice) UpdateRotation(value); } }
    uint _RotationAngle;
    VideoProcessorRotation _d3d11vpRotation  = VideoProcessorRotation.Identity;
    bool rotationLinesize; // if negative should be vertically flipped

    public bool             HFlip           { get => _HFlip;set { _HFlip = value; lock (lockDevice) UpdateRotation(_RotationAngle); } }
    bool _HFlip;

    public bool             VFlip           { get => _VFlip;set { _VFlip = value; lock (lockDevice) UpdateRotation(_RotationAngle); } }
    bool _VFlip;

    public VideoProcessors  VideoProcessor      { get => videoProcessor;    private set => SetUI(ref videoProcessor, value); }
    VideoProcessors videoProcessor = VideoProcessors.Flyleaf;

    /// <summary>
    /// Zoom percentage (100% equals to 1.0)
    /// </summary>
    public double              Zoom            { get => zoom;          set => SetZoom(value); }
    double zoom = 1;
    public void SetZoom(double zoom, bool refresh = true)
    {
        lock(lockDevice)
        {
            this.zoom = zoom;

            if (Disposed)
                return;

            SetViewport(refresh);
        }
    }

    public Point            ZoomCenter      { get => zoomCenter;    set => SetZoomCenter(value); }
    Point zoomCenter = ZoomCenterPoint;
    public static Point ZoomCenterPoint = new(0.5, 0.5);
    public void SetZoomCenter(Point p, bool refresh = true)
    {
        lock(lockDevice)
        {
            zoomCenter = p;

            if (Disposed)
                return;

            if (refresh)
                SetViewport();
        }
    }
    public void SetZoomAndCenter(double zoom, Point p, bool refresh = true)
    {
        lock(lockDevice)
        {
            this.zoom = zoom;
            zoomCenter = p;

            if (Disposed)
                return;

            if (refresh)
                SetViewport();
        }
    }
    public void SetPanAll(int panX, int panY, uint rotation, double zoom, Point p, bool refresh = true)
    {
        lock(lockDevice)
        {
            panXOffset = panX;
            panYOffset = panY;
            this.zoom = zoom;
            zoomCenter = p;
            UpdateRotation(rotation, false);

            if (Disposed)
                return;

            if (refresh)
                SetViewport();
        }
    }

    public int              UniqueId        { get; private set; }

    public Dictionary<VideoFilters, VideoFilter>
                            Filters         { get; set; }
    public VideoFrame       LastFrame       { get; set; }
    public RawRect          VideoRect       { get; set; }

    LogHandler Log;

    public Renderer(VideoDecoder videoDecoder, IntPtr handle = new IntPtr(), int uniqueId = -1)
    {
        UniqueId    = uniqueId == -1 ? Utils.GetUniqueId() : uniqueId;
        VideoDecoder= videoDecoder;
        Config      = videoDecoder.Config;
        Log         = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + " [Renderer      ] ");

        overlayTextureDesc = new()
        {
            Usage       = ResourceUsage.Default,
            Width       = 0,
            Height      = 0,
            Format      = Format.B8G8R8A8_UNorm,
            ArraySize   = 1,
            MipLevels   = 1,
            BindFlags   = BindFlags.ShaderResource,
            SampleDescription = new SampleDescription(1, 0)
        };

        singleStageDesc = new Texture2DDescription()
        {
            Usage       = ResourceUsage.Staging,
            Format      = Format.B8G8R8A8_UNorm,
            ArraySize   = 1,
            MipLevels   = 1,
            BindFlags   = BindFlags.None,
            CPUAccessFlags      = CpuAccessFlags.Read,
            SampleDescription   = new SampleDescription(1, 0),

            Width       = 0,
            Height      = 0
        };

        singleGpuDesc = new Texture2DDescription()
        {
            Usage       = ResourceUsage.Default,
            Format      = Format.B8G8R8A8_UNorm,
            ArraySize   = 1,
            MipLevels   = 1,
            BindFlags   = BindFlags.RenderTarget | BindFlags.ShaderResource,
            SampleDescription   = new SampleDescription(1, 0)
        };

        wndProcDelegate = new(WndProc);
        wndProcDelegatePtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
        ControlHandle = handle;
        Initialize();
    }

    #region Replica Renderer (Expiremental)
    public Renderer child; // allow access to child renderer (not safe)
    Renderer parent;
    public Renderer(Renderer renderer, IntPtr handle, int uniqueId = -1)
    {
        UniqueId            = uniqueId == -1 ? Utils.GetUniqueId() : uniqueId;
        Log                 = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + " [Renderer  Repl] ");

        renderer.child      = this;
        parent              = renderer;
        Config              = renderer.Config;
        wndProcDelegate     = new(WndProc);
        wndProcDelegatePtr  = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
        ControlHandle       = handle;
    }

    public void SetChildHandle(IntPtr handle)
    {
        lock (lockDevice)
        {
            if (child != null)
                DisposeChild();

            if (handle == IntPtr.Zero)
                return;

            child = new(this, handle, UniqueId);
            InitializeChildSwapChain();
        }
    }
    #endregion
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class FlyleafOverlay : UserControl
{
    private FlyleafOverlayVM VM => (FlyleafOverlayVM)DataContext;

    public FlyleafOverlay()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<FlyleafOverlayVM>();
    }

    private void FlyleafOverlay_OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (e.HeightChanged)
        {
            // The height of MainWindow cannot be used because it includes the title bar,
            // so the height is obtained here and passed on.
            double heightDiff = Math.Abs(e.NewSize.Height - e.PreviousSize.Height);

            if (heightDiff >= 1.0)
            {
                VM.FL.Config.ScreenWidth = e.NewSize.Width;
                VM.FL.Config.ScreenHeight = e.NewSize.Height;
            }
        }
    }
}

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;
using Whisper.net.Ggml;

namespace LLPlayer.ViewModels;

public class WhisperModelDownloadDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public WhisperModelDownloadDialogVM(FlyleafManager fl)
    {
        FL = fl;

        List<WhisperCppModel> models = WhisperCppModelLoader.LoadAllModels();
        foreach (var model in models)
        {
            Models.Add(model);
        }

        SelectedModel = Models.First();

        CmdDownloadModel!.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == nameof(CmdDownloadModel.IsExecuting))
            {
                OnPropertyChanged(nameof(CanDownload));
                OnPropertyChanged(nameof(CanDelete));
            }
        };
    }

    private const string TempExtension = ".tmp";

    public ObservableCollection<WhisperCppModel> Models { get; set => Set(ref field, value); } = new();

    public WhisperCppModel SelectedModel
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanDownload));
                OnPropertyChanged(nameof(CanDelete));
            }
        }
    }

    public string StatusText { get; set => Set(ref field, value); } = "Select a model to download.";

    public long DownloadedSize { get; set => Set(ref field, value); }

    public bool CanDownload =>
        SelectedModel is { Downloaded: false } && !CmdDownloadModel!.IsExecuting;

    public bool CanDelete =>
        SelectedModel is { Downloaded: true } && !CmdDownloadModel!.IsExecuting;

    private CancellationTokenSource? _cts;

    public AsyncDelegateCommand? CmdDownloadModel => field ??= new AsyncDelegateCommand(async () =>
    {
        _cts = new CancellationTokenSource();
        CancellationToken token = _cts.Token;

        WhisperCppModel downloadModel = SelectedModel;
        string tempModelPath = downloadModel.ModelFilePath + TempExtension;

        try
        {
            if (downloadModel.Downloaded)
            {
                StatusText = $"Model '{SelectedModel}' is already downloaded";
                return;
            }

            // Delete temporary files if they exist (forces re-download)
            if (!DeleteTempModel())
            {
                StatusText = $"Failed to remove temp model";
                return;
            }

            StatusText = $"Model '{downloadModel}' downloading..";

            long modelSize = await DownloadModelWithProgressAsync(downloadModel.Model, tempModelPath, token);

            // After successful download, rename temporary file to final file
            File.Move(tempModelPath, downloadModel.ModelFilePath);

            // Update downloaded status
            downloadModel.Size = modelSize;
            OnDownloadStatusChanged();

            StatusText = $"Model '{SelectedModel}' is downloaded successfully";
        }
        catch (OperationCanceledException)
        {
            StatusText = "Download canceled";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to download: {ex.Message}";
        }
        finally
        {
            _cts = null;
            DeleteTempModel();
        }

        return;

        bool DeleteTempModel()
        {
            // Delete temporary files if they exist
            if (File.Exists(tempModelPath))
            {
                try
                {
                    File.Delete(tempModelPath);
                }
                catch (Exception)
                {
                    // ignore

                    return false;
                }
            }

            return true;
        }
    }).ObservesCanExecute(() => CanDownload);

    public DelegateCommand? CmdCancelDownloadModel => field ??= new(() =>
    {
        _cts?.Cancel();
    });

    public DelegateCommand? CmdDeleteModel => field ??= new DelegateCommand(() =>
    {
        try
        {
            StatusText = $"Model '{SelectedModel}' deleting...";

            WhisperCppModel deleteModel = SelectedModel;

            // Delete model file if exists
            if (File.Exists(deleteModel.ModelFilePath))
            {
                File.Delete(deleteModel.ModelFilePath);
            }

            // Update downloaded status
            deleteModel.Size = 0;
            OnDownloadStatusChanged();

            StatusText = $"Model '{deleteModel}' is deleted successfully";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to delete model: {ex.Message}";
        }
    }).ObservesCanExecute(() => CanDelete);

    public DelegateCommand? CmdOpenFolder => field ??= new(() =>
    {
        if (!Directory.Exists(WhisperConfig.ModelsDirectory))
            return;

        try
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = WhisperConfig.ModelsDirectory,
                UseShellExecute = true,
                CreateNoWindow = true
            });
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Failed to open folder: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    });

    private void OnDownloadStatusChanged()
    {
        OnPropertyChanged(nameof(CanDownload));
        OnPropertyChanged(nameof(CanDelete));
    }

    private async Task<long> DownloadModelWithProgressAsync(GgmlType modelType, string destinationPath, CancellationToken token)
    {
        DownloadedSize = 0;

        await using Stream modelStream = await WhisperGgmlDownloader.Default.GetGgmlModelAsync(modelType, default, token);
        await using FileStream fileWriter = File.OpenWrite(destinationPath);

        byte[] buffer = new byte[1024 * 128];
        int bytesRead;
        long totalBytesRead = 0;

        Stopwatch sw = new();
        sw.Start();

        while ((bytesRead = await modelStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
        {
            await fileWriter.WriteAsync(buffer, 0, bytesRead, token);
            totalBytesRead += bytesRead;

            if (sw.Elapsed > TimeSpan.FromMilliseconds(50))
            {
                DownloadedSize = totalBytesRead;
                sw.Restart();
            }

            token.ThrowIfCancellationRequested();
        }

        return totalBytesRead;
    }

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); } = $"Whisper Downloader - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 400;
    public double WindowHeight { get; set => Set(ref field, value); } = 200;

    public bool CanCloseDialog() => !CmdDownloadModel!.IsExecuting;
    public void OnDialogClosed() { }
    public void OnDialogOpened(IDialogParameters parameters) { }
    public DialogCloseListener RequestClose { get; }
    #endregion IDialogAware
}

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using FlyleafLib.MediaPlayer.Translation;
using FlyleafLib.MediaPlayer.Translation.Services;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.Controls;

public partial class WordPopup : UserControl, INotifyPropertyChanged
{
    public FlyleafManager FL { get; }
    private ITranslateService? _translateService;
    private readonly TranslateServiceFactory _translateServiceFactory;
    private PDICSender? _pdicSender;
    private readonly Lock _locker = new();

    private string _clickedWords = string.Empty;
    private string _clickedText = string.Empty;

    private CancellationTokenSource? _cts;
    private readonly Dictionary<string, string> _translateCache = new();
    private string? _lastSearchActionUrl;

    public WordPopup()
    {
        InitializeComponent();

        FL = ((App)Application.Current).Container.Resolve<FlyleafManager>();

        FL.Player.PropertyChanged += Player_OnPropertyChanged;

        _translateServiceFactory = new TranslateServiceFactory(FL.PlayerConfig.Subtitles);

        FL.PlayerConfig.Subtitles.PropertyChanged += SubtitlesOnPropertyChanged;
        FL.PlayerConfig.Subtitles.TranslateChatConfig.PropertyChanged += ChatConfigOnPropertyChanged;
        FL.Player.SubtitlesManager[0].PropertyChanged += SubManagerOnPropertyChanged;
        FL.Player.SubtitlesManager[1].PropertyChanged += SubManagerOnPropertyChanged;
        FL.Config.Subs.PropertyChanged += SubsOnPropertyChanged;

        InitializeContextMenu();
    }

    public bool IsLoading { get; set => Set(ref field, value); }

    public bool IsOpen { get; set => Set(ref field, value); }

    public UIElement? PopupPlacementTarget { get; set => Set(ref field, value); }

    public double PopupHorizontalOffset { get; set => Set(ref field, value); }

    public double PopupVerticalOffset { get; set => Set(ref field, value); }

    public ContextMenu? PopupContextMenu { get; set => Set(ref field, value); }
    public ContextMenu? WordContextMenu { get; set => Set(ref field, value); }

    public bool IsSidebar { get; set; }

    private void SubtitlesOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(Config.SubtitlesConfig.TranslateWordServiceType):
            case nameof(Config.SubtitlesConfig.TranslateTargetLanguage):
            case nameof(Config.SubtitlesConfig.LanguageFallbackPrimary):
                // Apply translating settings changes
                Clear();
                break;
        }
    }

    private void ChatConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        // Apply translating settings changes
        Clear();
    }

    private void SubManagerOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(SubManager.Language))
        {
            // Apply source language changes
            Clear();
        }
    }

    private void SubsOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(AppConfigSubs.WordMenuActions))
        {
            // Apply word action settings changes
            InitializeContextMenu();
        }
    }

    private void Clear()
    {
        _translateService?.Dispose();
        _translateService = null;
        // clear cache
        _translateCache.Clear();
    }

    private void InitializeContextMenu()
    {
        var contextMenuStyle = (Style)FindResource("FlyleafContextMenu");

        ContextMenu popupMenu = new()
        {
            Placement = PlacementMode.Mouse,
            Style = contextMenuStyle
        };

        SetupContextMenu(popupMenu);
        PopupContextMenu = popupMenu;

        ContextMenu wordMenu = new()
        {
            Placement = PlacementMode.Mouse,
            Style = contextMenuStyle
        };

        SetupContextMenu(wordMenu);
        WordContextMenu = wordMenu;
    }

    private void SetupContextMenu(ContextMenu contextMenu)
    {
        IEnumerable<IMenuAction> actions = FL.Config.Subs.WordMenuActions.Where(a => a.IsEnabled);
        foreach (IMenuAction action in actions)
        {
            MenuItem menuItem = new() { Header = action.Title };

            // Initialize default action at the top
            // TODO: L: want to make the default action bold.
            if (_lastSearchActionUrl == null && action is SearchMenuAction sa)
            {
                // Only word search available
                if (sa.Url.Contains("%w") || sa.Url.Contains("%lw"))
                {
                    _lastSearchActionUrl = sa.Url;
                }
            }

            menuItem.Click += (o, args) =>
            {
                if (action is SearchMenuAction searchAction)
                {
                    // Only word search available
                    if (searchAction.Url.Contains("%w") || searchAction.Url.Contains("%lw"))
                    {
                        _lastSearchActionUrl = searchAction.Url;
                    }

                    OpenWeb(searchAction.Url, _clickedWords, _clickedText);
                }
                else if (action is ClipboardMenuAction clipboardAction)
                {
                    CopyToClipboard(_clickedWords, clipboardAction.ToLower);
                }
                else if (action is ClipboardAllMenuAction clipboardAllAction)
                {
                    CopyToClipboard(_clickedText, clipboardAllAction.ToLower);
                }
            };
            contextMenu.Items.Add(menuItem);
        }
    }

    // Click on video screen to close pop-up
    private void Player_OnPropertyChanged(object? sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == nameof(FL.Player.Status) && FL.Player.Status == Status.Playing)
        {
            IsOpen = false;
        }
    }

    private async ValueTask<string> TranslateWithCache(string text, WordClickedEventArgs e, CancellationToken token)
    {
        // already translated by translator
        if (e.IsTranslated)
        {
            return text;
        }

        var srcLang = FL.Player.SubtitlesManager[e.SubIndex].Language;
        var targetLang = FL.PlayerConfig.Subtitles.TranslateTargetLanguage;

        // Not set language or same language
        if (srcLang == null || srcLang.ISO6391 == targetLang.ToISO6391())
        {
            return text;
        }

        string lower = text.ToLower();
        if (_translateCache.TryGetValue(lower, out var cache))
        {
            return cache;
        }

        if (_translateService == null)
        {
            try
            {
                var service = _translateServiceFactory.GetService(FL.PlayerConfig.Subtitles.TranslateWordServiceType, true);
                service.Initialize(srcLang, targetLang);
                _translateService = service;
            }
            catch (TranslationConfigException ex)
            {
                Clear();
                ErrorDialogHelper.ShowKnownErrorPopup(ex.Message, KnownErrorType.Configuration);

                return text;
            }
        }

        try
        {
            string result = await _translateService.TranslateAsync(text, token);
            _translateCache.TryAdd(lower, result);

            return result;
        }
        catch (TranslationException ex)
        {
            ErrorDialogHelper.ShowUnknownErrorPopup(ex.Message, UnknownErrorType.Translation, ex);

            return text;
        }
    }

    public async Task OnWordClicked(WordClickedEventArgs e)
    {
        _clickedWords = e.Words;
        _clickedText = e.Text;

        if (FL.Player.Status == Status.Playing)
        {
            FL.Player.Pause();
        }

        if (e.Mouse is MouseClick.Left or MouseClick.Middle)
        {
            switch (FL.Config.Subs.WordClickActionMethod)
            {
                case WordClickAction.Clipboard:
                    CopyToClipboard(e.Words);
                    break;

                case WordClickAction.ClipboardAll:
                    CopyToClipboard(e.Text);
                    break;

                default:
                    await Popup(e);
                    break;
            }
        }
        else if (e.Mouse == MouseClick.Right)
        {
            WordContextMenu!.IsOpen = true;
        }
    }

    private async Task Popup(WordClickedEventArgs e)
    {
        if (FL.Config.Subs.WordCopyOnSelected)
        {
            CopyToClipboard(e.Words);
        }

        if (FL.Config.Subs.WordClickActionMethod == WordClickAction.PDIC && e.IsWord)
        {
            if (_pdicSender == null)
            {
                // Initialize PDIC lazily
                lock (_locker)
                {
                    _pdicSender ??= ((App)Application.Current).Container.Resolve<PDICSender>();
                }
            }

            _ = _pdicSender.SendWithPipe(e.Text, e.WordOffset + 1);
            return;
        }

        if (_cts != null)
        {
            // Canceled if running ahead
            _cts.Cancel();
            _cts.Dispose();
        }

        _cts = new CancellationTokenSource();

        string source = e.Words;

        IsLoading = true;

        SourceText.Text = source;
        TranslationText.Text = "";

        if (IsSidebar && e.Sender is SelectableTextBox)
        {
            var listBoxItem = UIHelper.FindParent<ListBoxItem>(e.Sender);
            if (listBoxItem != null)
            {
                PopupPlacementTarget = listBoxItem;
            }
        }

        if (FL.Config.Subs.WordLastSearchOnSelected)
        {
            if (Keyboard.Modifiers == FL.Config.Subs.WordLastSearchOnSelectedModifier)
            {
                if (_lastSearchActionUrl != null)
                {
                    OpenWeb(_lastSearchActionUrl, source);
                }
            }
        }

        IsOpen = true;

        await UpdatePosition();

        try
        {
            string result = await TranslateWithCache(source, e, _cts.Token);
            TranslationText.Text = result;
            IsLoading = false;
        }
        catch (OperationCanceledException)
        {
            return;
        }

        await UpdatePosition();

        return;

        async Task UpdatePosition()
        {
            // ActualWidth is updated asynchronously, so it needs to be offloaded in the Dispatcher.
            await Dispatcher.BeginInvoke(() =>
            {
                if (IsSidebar && PopupPlacementTarget != null)
                {
                    // for sidebar
                    PopupVerticalOffset = (((ListBoxItem)PopupPlacementTarget).ActualHeight - ActualHeight) / 2;
                    PopupHorizontalOffset = -ActualWidth - 10;
                }
                else
                {
                    // for subtitle
                    PopupHorizontalOffset = e.WordsX + ((e.WordsWidth - ActualWidth) / 2);
                    PopupVerticalOffset = -ActualHeight;
                }

            }, DispatcherPriority.Background);
        }
    }

    private void CloseButton_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        IsOpen = false;
    }

    private void SourceText_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (_lastSearchActionUrl != null)
        {
            OpenWeb(_lastSearchActionUrl, SourceText.Text);
        }
    }

    private static void CopyToClipboard(string text, bool toLower = false)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            return;
        }

        if (toLower)
        {
            text = text.ToLower();
        }

        // copy word
        try
        {
            // slow (10ms)
            //Clipboard.SetText(text);

            WindowsClipboard.SetText(text);
        }
        catch
        {
            // ignored
        }
    }

    private static void OpenWeb(string url, string words, string sentence = "")
    {
        if (url.Contains("%lw"))
        {
            url = url.Replace("%lw", Uri.EscapeDataString(words.ToLower()));
        }

        if (url.Contains("%w"))
        {
            url = url.Replace("%w", Uri.EscapeDataString(words));
        }

        if (url.Contains("%s"))
        {
            url = url.Replace("%s", Uri.EscapeDataString(sentence));
        }

        Process.Start(new ProcessStartInfo
        {
            FileName = url,
            UseShellExecute = true
        });
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    #endregion
}

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.ViewModels;

// TODO: L: consider commonization with WhisperModelDownloadDialogVM
public class TesseractDownloadDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public TesseractDownloadDialogVM(FlyleafManager fl)
    {
        FL = fl;

        List<TesseractModel> models = TesseractModelLoader.LoadAllModels();
        foreach (var model in models)
        {
            Models.Add(model);
        }

        SelectedModel = Models.First();

        CmdDownloadModel!.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == nameof(CmdDownloadModel.IsExecuting))
            {
                OnPropertyChanged(nameof(CanDownload));
                OnPropertyChanged(nameof(CanDelete));
            }
        };
    }

    private const string TempExtension = ".tmp";

    public ObservableCollection<TesseractModel> Models { get; set => Set(ref field, value); } = new();

    public TesseractModel SelectedModel
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanDownload));
                OnPropertyChanged(nameof(CanDelete));
            }
        }
    }

    public string StatusText { get; set => Set(ref field, value); } = "Select a model to download.";

    public long DownloadedSize { get; set => Set(ref field, value); }

    public bool CanDownload =>
        SelectedModel is { Downloaded: false } && !CmdDownloadModel!.IsExecuting;

    public bool CanDelete =>
        SelectedModel is { Downloaded: true } && !CmdDownloadModel!.IsExecuting;

    private CancellationTokenSource? _cts;

    public AsyncDelegateCommand? CmdDownloadModel => field ??= new AsyncDelegateCommand(async () =>
    {
        _cts = new CancellationTokenSource();
        CancellationToken token = _cts.Token;

        TesseractModel downloadModel = SelectedModel;
        string tempModelPath = downloadModel.ModelFilePath + TempExtension;

        try
        {
            if (downloadModel.Downloaded)
            {
                StatusText = $"Model '{SelectedModel}' is already downloaded";
                return;
            }

            // Delete temporary files if they exist (forces re-download)
            if (!DeleteTempModel())
            {
                StatusText = "Failed to remove temp model";
                return;
            }

            StatusText = $"Model '{downloadModel}' downloading..";

            long modelSize = await DownloadModelWithProgressAsync(downloadModel.LangCode, tempModelPath, token);

            // After successful download, rename temporary file to final file
            File.Move(tempModelPath, downloadModel.ModelFilePath);

            // Update downloaded status
            downloadModel.Size = modelSize;
            OnDownloadStatusChanged();

            StatusText = $"Model '{SelectedModel}' is downloaded successfully";
        }
        catch (OperationCanceledException ex)
            when (!ex.Message.StartsWith("The request was canceled due to the configured HttpClient.Timeout"))
        {
            StatusText = "Download canceled";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to download: {ex.Message}";
        }
        finally
        {
            _cts = null;
            DeleteTempModel();
        }

        return;

        bool DeleteTempModel()
        {
            // Delete temporary files if they exist
            if (File.Exists(tempModelPath))
            {
                try
                {
                    File.Delete(tempModelPath);
                }
                catch (Exception)
                {
                    // ignore

                    return false;
                }
            }

            return true;
        }
    }).ObservesCanExecute(() => CanDownload);

    public DelegateCommand? CmdCancelDownloadModel => field ??= new(() =>
    {
        _cts?.Cancel();
    });

    public DelegateCommand? CmdDeleteModel => field ??= new DelegateCommand(() =>
    {
        try
        {
            StatusText = $"Model '{SelectedModel}' deleting...";

            TesseractModel deleteModel = SelectedModel;

            // Delete model file if exists
            if (File.Exists(deleteModel.ModelFilePath))
            {
                File.Delete(deleteModel.ModelFilePath);
            }

            // Update downloaded status
            deleteModel.Size = 0;
            OnDownloadStatusChanged();

            StatusText = $"Model '{deleteModel}' is deleted successfully";
        }
        catch (Exception ex)
        {
            StatusText = $"Failed to delete model: {ex.Message}";
        }
    }).ObservesCanExecute(() => CanDelete);

    public DelegateCommand? CmdOpenFolder => field ??= new(() =>
    {
        if (!Directory.Exists(TesseractModel.ModelsDirectory))
            return;

        Process.Start(new ProcessStartInfo
        {
            FileName = TesseractModel.ModelsDirectory,
            UseShellExecute = true,
            CreateNoWindow = true
        });
    });

    private void OnDownloadStatusChanged()
    {
        OnPropertyChanged(nameof(CanDownload));
        OnPropertyChanged(nameof(CanDelete));
    }

    private async Task<long> DownloadModelWithProgressAsync(string langCode, string destinationPath, CancellationToken token)
    {
        DownloadedSize = 0;

        using HttpClient httpClient = new();
        httpClient.Timeout = TimeSpan.FromSeconds(10);

        using var response = await httpClient.GetAsync($"https://github.com/tesseract-ocr/tessdata/raw/refs/heads/main/{langCode}.traineddata", HttpCompletionOption.ResponseHeadersRead, token);

        response.EnsureSuccessStatusCode();

        await using Stream modelStream = await response.Content.ReadAsStreamAsync(token);
        await using FileStream fileWriter = File.OpenWrite(destinationPath);

        byte[] buffer = new byte[1024 * 128];
        int bytesRead;
        long totalBytesRead = 0;

        Stopwatch sw = new();
        sw.Start();

        while ((bytesRead = await modelStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
        {
            await fileWriter.WriteAsync(buffer, 0, bytesRead, token);
            totalBytesRead += bytesRead;

            if (sw.Elapsed > TimeSpan.FromMilliseconds(50))
            {
                DownloadedSize = totalBytesRead;
                sw.Restart();
            }

            token.ThrowIfCancellationRequested();
        }

        return totalBytesRead;
    }

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); } = $"Tesseract Downloader - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 400;
    public double WindowHeight { get; set => Set(ref field, value); } = 200;

    public bool CanCloseDialog() => !CmdDownloadModel!.IsExecuting;
    public void OnDialogClosed() { }
    public void OnDialogOpened(IDialogParameters parameters) { }
    public DialogCloseListener RequestClose { get; }
    #endregion IDialogAware
}

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using FlyleafLib.Controls.WPF;

namespace FlyleafLib.MediaPlayer.Translation.Services;

public interface ITranslateSettings : INotifyPropertyChanged;

public class GoogleV1TranslateSettings : NotifyPropertyChanged, ITranslateSettings
{
    private const string DefaultEndpoint = "https://translate.googleapis.com";

    public string Endpoint
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                CmdSetDefaultEndpoint.OnCanExecuteChanged();
            }
        }
    } = DefaultEndpoint;

    [JsonIgnore]
    public RelayCommand CmdSetDefaultEndpoint => field ??= new(_ =>
    {
        Endpoint = DefaultEndpoint;
    }, _ => Endpoint != DefaultEndpoint);

    public int TimeoutMs { get; set => Set(ref field, value); } = 10000;

    public Dictionary<string, string> Regions { get; set; } = new(GoogleV1TranslateService.DefaultRegions);

    /// <summary>
    /// for Settings
    /// </summary>
    [JsonIgnore]
    public ObservableCollection<LanguageRegions> LanguageRegions
    {
        get
        {
            if (field == null)
            {
                field = LoadLanguageRegions();

                foreach (var pref in field)
                {
                    pref.PropertyChanged += (sender, args) =>
                    {
                        if (args.PropertyName == nameof(Services.LanguageRegions.SelectedRegionMember))
                        {
                            // Apply changes in setting
                            Regions[pref.ISO6391] = pref.SelectedRegionMember.Code;
                        }
                    };
                }
            }

            return field;
        }
    }

    private ObservableCollection<LanguageRegions> LoadLanguageRegions()
    {
        List<LanguageRegions> preferences = [
            new()
            {
                Name = "Chinese",
                ISO6391 = "zh",
                Regions =
                [
                    // priority to the above
                    new LanguageRegionMember { Name = "Chinese (Simplified)", Code = "zh-CN" },
                    new LanguageRegionMember { Name = "Chinese (Traditional)", Code = "zh-TW" }
                ],
            },
            new()
            {
                Name = "French",
                ISO6391 = "fr",
                Regions =
                [
                    new LanguageRegionMember { Name = "French (French)", Code = "fr-FR" },
                    new LanguageRegionMember { Name = "French (Canadian)", Code = "fr-CA" }
                ],
            },
            new()
            {
                Name = "Portuguese",
                ISO6391 = "pt",
                Regions =
                [
                    new LanguageRegionMember { Name = "Portuguese (Portugal)", Code = "pt-PT" },
                    new LanguageRegionMember { Name = "Portuguese (Brazil)", Code = "pt-BR" }
                ],
            }
        ];

        foreach (LanguageRegions p in preferences)
        {
            if (Regions.TryGetValue(p.ISO6391, out string code))
            {
                // loaded from config
                p.SelectedRegionMember = p.Regions.FirstOrDefault(r => r.Code == code);
            }
            else
            {
                // select first
                p.SelectedRegionMember = p.Regions.First();
            }
        }

        return new ObservableCollection<LanguageRegions>(preferences);
    }
}

public class DeepLTranslateSettings : NotifyPropertyChanged, ITranslateSettings
{
    public string ApiKey { get; set => Set(ref field, value); }

    public int TimeoutMs { get; set => Set(ref field, value); } = 10000;
}

public class DeepLXTranslateSettings : NotifyPropertyChanged, ITranslateSettings
{
    public string Endpoint { get; set => Set(ref field, value); } = "http://127.0.0.1:1188";

    public int TimeoutMs { get; set => Set(ref field, value); } = 10000;
}

public abstract class OpenAIBaseTranslateSettings : NotifyPropertyChanged, ITranslateSettings
{
    protected OpenAIBaseTranslateSettings()
    {
        // ReSharper disable once VirtualMemberCallInConstructor
        Endpoint = DefaultEndpoint;
    }

    public abstract TranslateServiceType ServiceType { get; }
    public string Endpoint
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                CmdSetDefaultEndpoint.OnCanExecuteChanged();
            }
        }
    }
    [JsonIgnore]
    protected virtual bool ReuseConnection => true;

    public abstract string DefaultEndpoint { get; }

    [JsonIgnore]
    public virtual string ChatPath
    {
        get => "/v1/chat/completions";
        set => throw new NotImplementedException();
    }

    public string Model { get; set => Set(ref field, value); }

    [JsonIgnore]
    public virtual bool ModelRequired => true;

    [JsonIgnore]
    public virtual bool ReasonStripRequired => true;

    public int TimeoutMs { get; set => Set(ref field, value); } = 15000;
    public int TimeoutHealthMs { get; set => Set(ref field, value); } = 2000;

    #region LLM Parameters
    public double Temperature
    {
        get;
        set
        {
            if (value is >= 0.0 and <= 2.0)
            {
                Set(ref field, Math.Round(value, 2));
            }
        }
    } = 0.0;

    public bool TemperatureManual { get; set => Set(ref field, value); } = true;

    public double TopP
    {
        get;
        set
        {
            if (value is >= 0.0 and <= 1.0)
            {
                Set(ref field, Math.Round(value, 2));
            }
        }
    } = 1;

    public bool TopPManual { get; set => Set(ref field, value); }

    public int? MaxTokens
    {
        get;
        set => Set(ref field, value is <= 0 ? null : value);
    }

    public int? MaxCompletionTokens
    {
        get;
        set => Set(ref field, value is <= 0 ? null : value);
    }
    #endregion

    /// <summary>
    /// GetHttpClient
    /// </summary>
    /// <param name="healthCheck"></param>
    /// <returns></returns>
    /// <exception cref="TranslationConfigException"></exception>
    internal virtual HttpClient GetHttpClient(bool healthCheck = false)
    {
        if (string.IsNullOrWhiteSpace(Endpoint))
        {
            throw new TranslationConfigException(
                $"Endpoint for {ServiceType} is not configured.");
        }

        if (!healthCheck)
        {
            if (ModelRequired && string.IsNullOrWhiteSpace(Model))
            {
                throw new TranslationConfigException(
                    $"Model for {ServiceType} is not configured.");
            }
        }

        // In KoboldCpp, if this is not set, even if it is sent with Connection: close,
        // the connection will be reused and an error will occur.
        HttpMessageHandler handler = ReuseConnection ?
            new HttpClientHandler() :
            new SocketsHttpHandler
            {
                PooledConnectionLifetime = TimeSpan.Zero,
                PooledConnectionIdleTimeout = TimeSpan.Zero,
            };

        HttpClient client = new(handler);
        client.BaseAddress = new Uri(Endpoint);
        client.Timeout = TimeSpan.FromMilliseconds(healthCheck ? TimeoutHealthMs : TimeoutMs);
        if (!ReuseConnection)
        {
            client.DefaultRequestHeaders.ConnectionClose = true;
        }

        return client;
    }

    #region For Settings
    [JsonIgnore]
    public ObservableCollection<string> AvailableModels { get; } = new();

    [JsonIgnore]
    public string Status
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(StatusAvailable));
            }
        }
    }

    [JsonIgnore]
    public bool StatusAvailable => !string.IsNullOrEmpty(Status);

    [JsonIgnore]
    public RelayCommand CmdSetDefaultEndpoint => field ??= new(_ =>
    {
        Endpoint = DefaultEndpoint;
    }, _ => Endpoint != DefaultEndpoint);

    [JsonIgnore]
    public RelayCommand CmdCheckEndpoint => new(async void (_) =>
    {
        try
        {
            Status = "Checking...";
            await LoadModels();
            Status = "OK";
        }
        catch (Exception ex)
        {
            Status = GetErrorDetails($"NG: {ex.Message}", ex);
        }
    });

    [JsonIgnore]
    public RelayCommand CmdGetModels => new(async void (_) =>
    {
        try
        {
            Status = "Checking...";
            await LoadModels();
            Status = ""; // clear
        }
        catch (Exception ex)
        {
            Status = GetErrorDetails($"NG: {ex.Message}", ex);
        }
    });

    [JsonIgnore]
    public RelayCommand CmdHelloModel => new(async void (_) =>
    {
        Stopwatch sw = new();
        sw.Start();
        try
        {
            Status = "Waiting...";

            await OpenAIBaseTranslateService.Hello(this);

            Status = $"OK in {sw.Elapsed.TotalSeconds} secs";
        }
        catch (Exception ex)
        {
            Status = GetErrorDetails($"NG in {sw.Elapsed.TotalSeconds} secs: {ex.Message}", ex);
        }
    });

    private async Task LoadModels()
    {
        string prevModel = Model;
        AvailableModels.Clear();

        var models = await OpenAIBaseTranslateService.GetLoadedModels(this);
        foreach (var model in models)
        {
            AvailableModels.Add(model);
        }

        if (!string.IsNullOrEmpty(prevModel))
        {
            Model = AvailableModels.FirstOrDefault(m => m == prevModel);
        }
    }

    internal static string GetErrorDetails(string header, Exception ex)
    {
        StringBuilder sb = new();
        sb.Append(header);

        if (ex.Data.Contains("status_code") && (string)ex.Data["status_code"] != "-1")
        {
            sb.AppendLine();
            sb.AppendLine();
            sb.Append($"status_code: {ex.Data["status_code"]}");
        }

        if (ex.Data.Contains("response") && (string)ex.Data["response"] != "")
        {
            sb.AppendLine();
            sb.Append($"response: {ex.Data["response"]}");
        }

        return sb.ToString();
    }
    #endregion
}

public class OllamaTranslateSettings : OpenAIBaseTranslateSettings
{
    public OllamaTranslateSettings()
    {
        TimeoutMs = 20000;
    }

    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.Ollama;
    [JsonIgnore]
    public override string DefaultEndpoint => "http://127.0.0.1:11434";
}

public class LMStudioTranslateSettings : OpenAIBaseTranslateSettings
{
    public LMStudioTranslateSettings()
    {
        TimeoutMs = 20000;
    }

    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.LMStudio;
    [JsonIgnore]
    public override string DefaultEndpoint => "http://127.0.0.1:1234";
    [JsonIgnore]
    public override bool ModelRequired => false;
}

public class KoboldCppTranslateSettings : OpenAIBaseTranslateSettings
{
    public KoboldCppTranslateSettings()
    {
        TimeoutMs = 20000;
    }

    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.KoboldCpp;
    [JsonIgnore]
    public override string DefaultEndpoint => "http://127.0.0.1:5001";

    // Disabled due to error when reusing connections
    [JsonIgnore]
    protected override bool ReuseConnection => false;

    [JsonIgnore]
    public override bool ModelRequired => false;
}

public class OpenAITranslateSettings : OpenAIBaseTranslateSettings
{
    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.OpenAI;
    [JsonIgnore]
    public override string DefaultEndpoint => "https://api.openai.com";
    public string ApiKey { get; set => Set(ref field, value); }

    [JsonIgnore]
    public override bool ReasonStripRequired => false;

    /// <summary>
    /// GetHttpClient
    /// </summary>
    /// <param name="healthCheck"></param>
    /// <returns></returns>
    /// <exception cref="TranslationConfigException"></exception>
    internal override HttpClient GetHttpClient(bool healthCheck = false)
    {
        if (string.IsNullOrWhiteSpace(ApiKey))
        {
            throw new TranslationConfigException(
                $"API Key for {ServiceType} is not configured.");
        }

        HttpClient client = base.GetHttpClient(healthCheck);
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");

        return client;
    }
}

public class OpenAILikeTranslateSettings : OpenAIBaseTranslateSettings
{
    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.OpenAILike;
    [JsonIgnore]
    public override string DefaultEndpoint => "https://api.openai.com";

    private const string DefaultChatPath = "/v1/chat/completions";
    public override string ChatPath
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                CmdSetDefaultChatPath.OnCanExecuteChanged();
            }
        }
    } = DefaultChatPath;

    [JsonIgnore]
    public RelayCommand CmdSetDefaultChatPath => field ??= new(_ =>
    {
        ChatPath = DefaultChatPath;
    }, _ => ChatPath != DefaultChatPath);

    [JsonIgnore]
    public override bool ModelRequired => false;
    public string ApiKey { get; set => Set(ref field, value); }

    /// <summary>
    /// GetHttpClient
    /// </summary>
    /// <param name="healthCheck"></param>
    /// <returns></returns>
    /// <exception cref="TranslationConfigException"></exception>
    internal override HttpClient GetHttpClient(bool healthCheck = false)
    {
        HttpClient client = base.GetHttpClient(healthCheck);

        // optional ApiKey
        if (!string.IsNullOrWhiteSpace(ApiKey))
        {
            client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
        }

        return client;
    }
}

public class ClaudeTranslateSettings : OpenAIBaseTranslateSettings
{
    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.Claude;
    [JsonIgnore]
    public override string DefaultEndpoint => "https://api.anthropic.com";
    public string ApiKey { get; set => Set(ref field, value); }

    [JsonIgnore]
    public override bool ReasonStripRequired => false;

    internal override HttpClient GetHttpClient(bool healthCheck = false)
    {
        if (string.IsNullOrWhiteSpace(ApiKey))
        {
            throw new TranslationConfigException(
                $"API Key for {ServiceType} is not configured.");
        }

        HttpClient client = base.GetHttpClient(healthCheck);
        client.DefaultRequestHeaders.Add("x-api-key", ApiKey);
        client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");

        return client;
    }
}

public class LiteLLMTranslateSettings : OpenAIBaseTranslateSettings
{
    [JsonIgnore]
    public override TranslateServiceType ServiceType => TranslateServiceType.LiteLLM;
    [JsonIgnore]
    public override string DefaultEndpoint => "http://127.0.0.1:4000";
}

public class LanguageRegionMember : NotifyPropertyChanged, IEquatable<LanguageRegionMember>
{
    public string Name { get; set; }
    public string Code { get; set; }

    public bool Equals(LanguageRegionMember other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        return Code == other.Code;
    }

    public override bool Equals(object obj) => obj is LanguageRegionMember o && Equals(o);

    public override int GetHashCode()
    {
        return (Code != null ? Code.GetHashCode() : 0);
    }
}

public class LanguageRegions : NotifyPropertyChanged
{
    public string ISO6391 { get; set; }
    public string Name { get; set; }
    public List<LanguageRegionMember> Regions { get; set; }

    public LanguageRegionMember SelectedRegionMember { get; set => Set(ref field, value); }
}

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using MaterialDesignThemes.Wpf;
using static FlyleafLib.Utils.NativeMethods;

namespace LLPlayer.ViewModels;

public class FlyleafOverlayVM : Bindable
{
    public FlyleafManager FL { get; }

    public FlyleafOverlayVM(FlyleafManager fl)
    {
        FL = fl;
    }

    public DelegateCommand? CmdOnLoaded => field ??= new(() =>
    {
        SetupOSDStatus();
        SetupFlyleafContextMenu();
        SetupFlyleafKeybindings();

        FL.Config.FlyleafHostLoaded();
    });

    private void SetupFlyleafContextMenu()
    {
        // When a context menu is opened using the Overlay.MouseRightButtonUp event,
        // it is a bubble event and therefore has priority over the ContextMenu of the child controls.
        // so set ContextMenu directly to each of the controls in order to give priority to the child contextmenu.
        ContextMenu menu = (ContextMenu)Application.Current.FindResource("PopUpMenu")!;
        menu.DataContext = this;
        menu.PlacementTarget = FL.FlyleafHost!.Overlay;

        FL.FlyleafHost!.Overlay.ContextMenu = menu;
        FL.FlyleafHost!.Surface.ContextMenu = menu;
    }

    #region Flyleaf Keybindings
    // TODO: L: Make it fully customizable like PotPlayer
    private void SetupFlyleafKeybindings()
    {
        // Subscribe to the same event to the following two
        // Surface: On Video Screen
        // Overlay: Content of FlyleafHost = FlyleafOverlay
        foreach (var window in (Window[])[FL.FlyleafHost!.Surface, FL.FlyleafHost!.Overlay])
        {
            window.MouseWheel += FlyleafOnMouseWheel;
        }
        FL.FlyleafHost.Surface.MouseUp += SurfaceOnMouseUp;
        FL.FlyleafHost.Surface.MouseDoubleClick += SurfaceOnMouseDoubleClick;
        FL.FlyleafHost.Surface.MouseLeftButtonDown += SurfaceOnMouseLeftButtonDown;
        FL.FlyleafHost.Surface.MouseLeftButtonUp += SurfaceOnMouseLeftButtonUp;
        FL.FlyleafHost.Surface.MouseMove += SurfaceOnMouseMove;
        FL.FlyleafHost.Surface.LostMouseCapture += SurfaceOnLostMouseCapture;
    }

    private void SurfaceOnMouseUp(object sender, MouseButtonEventArgs e)
    {
        switch (e.ChangedButton)
        {
            // Middle click: seek to current subtitle
            case MouseButton.Middle:
                bool ctrlDown = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
                if (ctrlDown)
                {
                    // CTRL + Middle click: Reset zoom
                    FL.Player.ResetZoom();
                    e.Handled = true;
                    return;
                }

                if (FL.Player.Subtitles[0].Enabled)
                {
                    FL.Player.Subtitles.CurSeek();
                }
                e.Handled = true;
                break;

            // X1 click: subtitles prev seek with fallback
            case MouseButton.XButton1:
                FL.Player.Subtitles.PrevSeekFallback();
                e.Handled = true;
                break;

            // X2 click: subtitles next seek with fallback
            case MouseButton.XButton2:
                FL.Player.Subtitles.NextSeekFallback();
                e.Handled = true;
                break;
        }
    }

    private void SurfaceOnMouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (FL.Config.MouseDoubleClickToFullScreen && e.ChangedButton == MouseButton.Left)
        {
            FL.Player.ToggleFullScreen();

            // Double and single clicks are not currently distinguished, so need to be re-fired
            // Timers need to be added to distinguish between the two,
            // but this is controversial because it delays a single click action
            SingleClickAction();

            e.Handled = true;
        }
    }

    private bool _isLeftDragging;
    private Point _downPoint;
    private Point _downCursorPoint;
    private long _downTick;

    private void SurfaceOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // Support window dragging & play / pause toggle
        if (Keyboard.Modifiers != ModifierKeys.None)
            return;

        if (FL.FlyleafHost is not { IsResizing: false, IsPanMoving: false, IsDragMoving: false })
            return;

        Point downPoint = FL.FlyleafHost!.Surface.PointToScreen(default);
        long downTick = Stopwatch.GetTimestamp();

        if (!FL.FlyleafHost.IsFullScreen && FL.FlyleafHost.Owner.WindowState == WindowState.Normal)
        {
            // normal window: window dragging
            DragMoveOwner();

            MouseLeftButtonUpAction(downPoint, downTick);
        }
        else
        {
            // fullscreen or maximized window: do action in KeyUp event
            _isLeftDragging = true;
            _downPoint = downPoint;
            _downTick = downTick;

            _downCursorPoint = e.GetPosition((IInputElement)sender);
        }
    }

    // When left dragging while maximized, move window back to normal and move window
    private void SurfaceOnMouseMove(object sender, MouseEventArgs e)
    {
        if (!_isLeftDragging || e.LeftButton != MouseButtonState.Pressed)
            return;

        if (FL.FlyleafHost!.IsFullScreen || FL.FlyleafHost.Owner.WindowState != WindowState.Maximized)
            return;

        Point curPoint = e.GetPosition((IInputElement)sender);

        // distinguish between mouse click and dragging (to prevent misfire)
        if (Math.Abs(curPoint.X - _downCursorPoint.X) >= 60 ||
            Math.Abs(curPoint.Y - _downCursorPoint.Y) >= 60)
        {
            _isLeftDragging = false;

            // change to normal window
            FL.FlyleafHost.Owner.WindowState = WindowState.Normal;

            // start dragging
            DragMoveOwner();
        }
    }

    private void DragMoveOwner()
    {
        // (!SeekBarShowOnlyMouseOver) always show cursor when moving
        // (SeekBarShowOnlyMouseOver)  prevent to activate seek bar
        if (!FL.Config.SeekBarShowOnlyMouseOver)
        {
            FL.Player.Activity.IsEnabled = false;
        }

        FL.FlyleafHost!.Owner.DragMove();

        if (!FL.Config.SeekBarShowOnlyMouseOver)
        {
            FL.Player.Activity.IsEnabled = true;
        }
    }

    private void SurfaceOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (!_isLeftDragging)
            return;

        _isLeftDragging = false;

        MouseLeftButtonUpAction(_downPoint, _downTick);
    }

    private void SurfaceOnLostMouseCapture(object sender, MouseEventArgs e)
    {
        _isLeftDragging = false;
    }

    private void MouseLeftButtonUpAction(Point downPoint, long downTick)
    {
        Point upPoint = FL.FlyleafHost!.Surface.PointToScreen(default);

        if (downPoint == upPoint // if not moved at all
            ||
            Stopwatch.GetElapsedTime(downTick) <= TimeSpan.FromMilliseconds(200)
            &&                   // if click within 200ms and not so much moved
            Math.Abs(upPoint.X - downPoint.X) < 60 && Math.Abs(upPoint.Y - downPoint.Y) < 60)
        {
            SingleClickAction();
        }
    }

    private void SingleClickAction()
    {
        // Toggle play/pause on left click
        if (FL.Config.MouseSingleClickToPlay && FL.Player.CanPlay)
            FL.Player.TogglePlayPause();
    }

    private void FlyleafOnMouseWheel(object sender, MouseWheelEventArgs e)
    {
        // CTRL + Wheel  : Zoom in / out
        // SHIFT + Wheel : Subtitles Up / Down
        // CTRL + SHIFT + Wheel : Subtitles Size Increase / Decrease
        if (e.Delta == 0)
        {
            return;
        }

        Window surface = FL.FlyleafHost!.Surface;

        bool ctrlDown = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
        bool shiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);

        if (ctrlDown && shiftDown)
        {
            if (e.Delta > 0)
            {
                FL.Action.CmdSubsSizeIncrease.Execute();
            }
            else
            {
                FL.Action.CmdSubsSizeDecrease.Execute();
            }
        }
        else if (ctrlDown)
        {
            Point cur = e.GetPosition(surface);
            Point curDpi = new(cur.X * DpiX, cur.Y * DpiY);
            if (e.Delta > 0)
            {
                FL.Player.ZoomIn(curDpi);
            }
            else
            {
                FL.Player.ZoomOut(curDpi);
            }
        }
        else if (shiftDown)
        {
            if (e.Delta > 0)
            {
                FL.Action.CmdSubsPositionUp.Execute();
            }
            else
            {
                FL.Action.CmdSubsPositionDown.Execute();
            }
        }
        else
        {
            if (FL.Config.MouseWheelToVolumeUpDown)
            {
                if (e.Delta > 0)
                {
                    FL.Player.Audio.VolumeUp();
                }
                else
                {
                    FL.Player.Audio.VolumeDown();
                }
            }
        }

        e.Handled = true;
    }
    #endregion

    #region OSD
    private void SetupOSDStatus()
    {
        var player = FL.Player;

        player.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(player.OSDMessage):
                    OSDMessage = player.OSDMessage;
                    break;
                case nameof(player.ReversePlayback):
                    OSDMessage = $"Reverse Playback {(player.ReversePlayback ? "On" : "Off")}";
                    break;
                case nameof(player.LoopPlayback):
                    OSDMessage = $"Loop Playback {(player.LoopPlayback ? "On" : "Off")}";
                    break;
                case nameof(player.Rotation):
                    OSDMessage = $"Rotation {player.Rotation}°";
                    break;
                case nameof(player.Speed):
                    OSDMessage = $"Speed x{player.Speed}";
                    break;
                case nameof(player.Zoom):
                    OSDMessage = $"Zoom {player.Zoom}%";
                    break;
                case nameof(player.Status):
                    // Change only Play and Pause to icon
                    switch (player.Status)
                    {
                        case Status.Paused:
                            OSDIcon = PackIconKind.Pause;
                            return;
                        case Status.Playing:
                            OSDIcon = PackIconKind.Play;
                            return;
                    }
                    OSDMessage = $"{player.Status.ToString()}";
                    break;
            }
        };

        player.Audio.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(player.Audio.Volume):
                    OSDMessage = $"Volume {player.Audio.Volume}%";
                    break;
                case nameof(player.Audio.Mute):
                    OSDIcon = player.Audio.Mute ? PackIconKind.VolumeOff : PackIconKind.VolumeHigh;
                    break;
            }
        };

        player.Config.Player.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(player.Config.Player.SeekAccurate):
                    OSDMessage = $"Always Seek Accurate {(player.Config.Player.SeekAccurate ? "On" : "Off")}";
                    break;
            }
        };

        player.Config.Audio.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(player.Config.Audio.Enabled):
                    OSDMessage = $"Audio {(player.Config.Audio.Enabled ? "Enabled" : "Disabled")}";
                    break;
                case nameof(player.Config.Audio.Delay):
                    OSDMessage = $"Audio Delay {player.Config.Audio.Delay / 10000}ms";
                    break;
            }
        };

        player.Config.Video.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(player.Config.Video.Enabled):
                    OSDMessage = $"Video {(player.Config.Video.Enabled ? "Enabled" : "Disabled")}";
                    break;
                case nameof(player.Config.Video.AspectRatio):
                    OSDMessage = $"Aspect Ratio {player.Config.Video.AspectRatio.ToString()}";
                    break;
                case nameof(player.Config.Video.VideoAcceleration):
                    OSDMessage = $"Video Acceleration {(player.Config.Video.VideoAcceleration ? "On" : "Off")}";
                    break;
            }
        };

        foreach (var (i, config) in player.Config.Subtitles.SubConfigs.Index())
        {
            config.PropertyChanged += (o, e) =>
            {
                switch (e.PropertyName)
                {
                    case nameof(config.Delay):
                        OSDMessage = $"{(i == 0 ? "Primary" : "Secondary")} Subs Delay {config.Delay / 10000}ms";
                        break;
                    case nameof(config.Visible):
                        var primary = player.Config.Subtitles[0].Visible ? "visible" : "hidden";
                        var secondary = player.Config.Subtitles[1].Visible ? "visible" : "hidden";

                        if (player.Subtitles[1].Enabled)
                        {
                            OSDMessage = $"Subs {primary} / {secondary}";
                        }
                        else
                        {
                            OSDMessage = $"Subs {primary}";
                        }
                        break;
                }
            };
        }

        // app config
        FL.Config.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(FL.Config.AlwaysOnTop):
                    OSDMessage = $"Always on Top {(FL.Config.AlwaysOnTop ? "On" : "Off")}";
                    break;
            }
        };

        FL.Config.Subs.PropertyChanged += (o, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(FL.Config.Subs.SubsAutoTextCopy):
                    OSDMessage = $"Subs Auto Text Copy {(FL.Config.Subs.SubsAutoTextCopy ? "On" : "Off")}";
                    break;
            }
        };
    }

    private CancellationTokenSource? _cancelMsgToken;

    public string OSDMessage
    {
        get => _osdMessage;
        set
        {
            if (Set(ref _osdMessage, value))
            {
                if (_cancelMsgToken != null)
                {
                    _cancelMsgToken.Cancel();
                    _cancelMsgToken.Dispose();
                    _cancelMsgToken = null;
                }

                if (Set(ref _osdIcon, null, nameof(OSDIcon)))
                {
                    OnPropertyChanged(nameof(IsOSDIcon));
                }

                _cancelMsgToken = new();

                var token = _cancelMsgToken.Token;
                _ = Task.Run(() => ClearOSD(3000, token));
            }
        }
    }
    private string _osdMessage = "";

    public PackIconKind? OSDIcon
    {
        get => _osdIcon;
        set
        {
            if (Set(ref _osdIcon, value))
            {
                OnPropertyChanged(nameof(IsOSDIcon));

                if (_cancelMsgToken != null)
                {
                    _cancelMsgToken.Cancel();
                    _cancelMsgToken.Dispose();
                    _cancelMsgToken = null;
                }

                Set(ref _osdMessage, "", nameof(OSDMessage));

                _cancelMsgToken = new();

                var token = _cancelMsgToken.Token;
                _ = Task.Run(() => ClearOSD(500, token));
            }
        }
    }
    private PackIconKind? _osdIcon;

    public bool IsOSDIcon => OSDIcon != null;

    private async Task ClearOSD(int timeoutMs, CancellationToken token)
    {
        await Task.Delay(timeoutMs, token);

        if (token.IsCancellationRequested)
            return;

        Utils.UI(() =>
        {
            Set(ref _osdMessage, "", nameof(OSDMessage));
            if (Set(ref _osdIcon, null, nameof(OSDIcon)))
            {
                OnPropertyChanged(nameof(IsOSDIcon));
            }
        });
    }
    #endregion
}

using System;
using System.Threading;
using System.Threading.Tasks;

using Vortice.DXGI;
using Vortice.Direct3D11;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaRenderer;

public unsafe partial class Renderer
{
    bool    isPresenting;
    long    lastPresentAt       = 0;
    long    lastPresentRequestAt= 0;
    object  lockPresentTask     = new();

    public bool Present(VideoFrame frame, bool forceWait = true)
    {
        if (Monitor.TryEnter(lockDevice, frame.timestamp == 0 ? 100 : 5)) // Allow more time for first frame
        {
            try
            {
                PresentInternal(frame, forceWait);
                VideoDecoder.DisposeFrame(LastFrame);
                LastFrame = frame;

                if (child != null)
                    child.LastFrame = frame;

                return true;

            }
            catch (Exception e)
            {
                if (CanWarn) Log.Warn($"Present frame failed {e.Message} | {Device?.DeviceRemovedReason}");
                VideoDecoder.DisposeFrame(frame);

                vpiv?.Dispose();

                return false;

            }
            finally
            {
                Monitor.Exit(lockDevice);
            }
        }

        if (CanDebug) Log.Debug("Dropped Frame - Lock timeout " + (frame != null ? Utils.TicksToTime(frame.timestamp) : ""));
        VideoDecoder.DisposeFrame(frame);

        return false;
    }
    public void Present()
    {
        if (SCDisposed)
            return;

        // NOTE: We don't have TimeBeginPeriod, FpsForIdle will not be accurate
        lock (lockPresentTask)
        {
            if ((Config.Player.player == null || !Config.Player.player.requiresBuffering) && VideoDecoder.IsRunning && (VideoStream == null || VideoStream.FPS > 10)) // With slow FPS we need to refresh as fast as possible
                return;

            if (isPresenting)
            {
                lastPresentRequestAt = DateTime.UtcNow.Ticks;
                return;
            }

            isPresenting = true;
        }

        Task.Run(() =>
        {
            long presentingAt;
            do
            {
                long sleepMs = DateTime.UtcNow.Ticks - lastPresentAt;
                sleepMs = sleepMs < (long)(1.0 / Config.Player.IdleFps * 1000 * 10000) ? (long) (1.0 / Config.Player.IdleFps * 1000) : 0;
                if (sleepMs > 2)
                    Thread.Sleep((int)sleepMs);

                presentingAt = DateTime.UtcNow.Ticks;
                RefreshLayout();
                lastPresentAt = DateTime.UtcNow.Ticks;

            } while (lastPresentRequestAt > presentingAt);

            isPresenting = false;
        });
    }
    internal void PresentInternal(VideoFrame frame, bool forceWait = true)
    {
        if (SCDisposed)
            return;

        // TBR: Replica performance issue with D3D11 (more zoom more gpu overload)
        if (frame.srvs == null) // videoProcessor can be FlyleafVP but the player can send us a cached frame from prev videoProcessor D3D11VP (check frame.srv instead of videoProcessor)
        {
            if (frame.avFrame != null)
            {
                vpivd.Texture2D.ArraySlice = (uint) frame.avFrame->data[1];
                vd1.CreateVideoProcessorInputView(VideoDecoder.textureFFmpeg, vpe, vpivd, out vpiv);
            }
            else
            {
                vpivd.Texture2D.ArraySlice = 0;
                vd1.CreateVideoProcessorInputView(frame.textures[0], vpe, vpivd, out vpiv);
            }

            vpsa[0].InputSurface = vpiv;
            vc.VideoProcessorBlt(vp, vpov, 0, 1, vpsa);
            swapChain.Present(Config.Video.VSync, forceWait ? PresentFlags.None : Config.Video.PresentFlags);

            vpiv.Dispose();
        }
        else
        {
            context.OMSetRenderTargets(backBufferRtv);
            context.ClearRenderTargetView(backBufferRtv, Config.Video._BackgroundColor);
            context.RSSetViewport(GetViewport);
            context.PSSetShaderResources(0, frame.srvs);
            context.Draw(6, 0);

            if (overlayTexture != null)
            {
                // Don't stretch the overlay (reduce height based on ratiox) | Sub's stream size might be different from video size (fix y based on percentage)
                var ratiox = (double)GetViewport.Width / overlayTextureOriginalWidth;
                var ratioy = (double)overlayTextureOriginalPosY / overlayTextureOriginalHeight;

                context.OMSetBlendState(blendStateAlpha);
                context.PSSetShaderResources(0, overlayTextureSRVs);
                context.RSSetViewport((float) (GetViewport.X + (overlayTextureOriginalPosX * ratiox)), (float) (GetViewport.Y + (GetViewport.Height * ratioy)), (float) (overlayTexture.Description.Width * ratiox), (float) (overlayTexture.Description.Height * ratiox));
                context.PSSetShader(ShaderBGRA);
                context.Draw(6, 0);

                // restore context
                context.PSSetShader(ShaderPS);
                context.OMSetBlendState(curPSCase == PSCase.RGBPacked ? blendStateAlpha : null);
            }

            swapChain.Present(Config.Video.VSync, forceWait ? PresentFlags.None : Config.Video.PresentFlags);
        }

        child?.PresentInternal(frame);
    }

    public void ClearOverlayTexture()
    {
        if (overlayTexture == null)
            return;

        overlayTexture?.Dispose();
        overlayTextureSrv?.Dispose();
        overlayTexture      = null;
        overlayTextureSrv   = null;
    }

    internal void CreateOverlayTexture(SubtitlesFrame frame, int streamWidth, int streamHeight)
    {
        var rect    = frame.sub.rects[0];
        var stride  = rect->linesize[0] * 4;

        overlayTextureOriginalWidth = streamWidth;
        overlayTextureOriginalHeight= streamHeight;
        overlayTextureOriginalPosX  = rect->x;
        overlayTextureOriginalPosY  = rect->y;
        overlayTextureDesc.Width    = (uint)rect->w;
        overlayTextureDesc.Height   = (uint)rect->h;

        byte[] data = ConvertBitmapSub(frame.sub, false);

        fixed(byte* ptr = data)
        {
            SubresourceData subData = new()
            {
                DataPointer = (nint)ptr,
                RowPitch    = (uint)stride
            };

            overlayTexture?.Dispose();
            overlayTextureSrv?.Dispose();
            overlayTexture          = Device.CreateTexture2D(overlayTextureDesc, new SubresourceData[] { subData });
            overlayTextureSrv       = Device.CreateShaderResourceView(overlayTexture);
            overlayTextureSRVs[0]   = overlayTextureSrv;
        }
    }

    public static byte[] ConvertBitmapSub(AVSubtitle sub, bool grey)
    {
        var rect = sub.rects[0];
        var stride = rect->linesize[0] * 4;

        byte[] data = new byte[rect->w * rect->h * 4];
        Span<uint> colors = stackalloc uint[256];

        fixed(byte* ptr = data)
        {
            var colorsData  = new Span<uint>((byte*)rect->data[1], rect->nb_colors);

            for (int i = 0; i < colorsData.Length; i++)
                colors[i] = colorsData[i];

            ConvertPal(colors, 256, grey);

            for (int y = 0; y < rect->h; y++)
            {
                uint* xout =(uint*) (ptr + y * stride);
                byte* xin = ((byte*)rect->data[0]) + y * rect->linesize[0];

                for (int x = 0; x < rect->w; x++)
                    *xout++ = colors[*xin++];
            }
        }

        return data;
    }

    static void ConvertPal(Span<uint> colors, int count, bool gray) // subs bitmap (source: mpv)
    {
        for (int n = 0; n < count; n++)
        {
            uint c = colors[n];
            uint b = c & 0xFF;
            uint g = (c >> 8) & 0xFF;
            uint r = (c >> 16) & 0xFF;
            uint a = (c >> 24) & 0xFF;

            if (gray)
                r = g = b = (r + g + b) / 3;

            // from straight to pre-multiplied alpha
            b = b * a / 255;
            g = g * a / 255;
            r = r * a / 255;
            colors[n] = b | (g << 8) | (r << 16) | (a << 24);
        }
    }

    public void RefreshLayout()
    {
        if (Monitor.TryEnter(lockDevice, 5))
        {
            try
            {
                if (SCDisposed)
                    return;

                if (LastFrame != null && (LastFrame.textures != null || LastFrame.avFrame != null))
                    PresentInternal(LastFrame);
                else if (Config.Video.ClearScreen)
                {
                    context.ClearRenderTargetView(backBufferRtv, Config.Video._BackgroundColor);
                    swapChain.Present(Config.Video.VSync, PresentFlags.None);
                }
            }
            catch (Exception e)
            {
                if (CanWarn) Log.Warn($"Present idle failed {e.Message} | {Device.DeviceRemovedReason}");
            }
            finally
            {
                Monitor.Exit(lockDevice);
            }
        }
    }
    public void ClearScreen()
    {
        ClearOverlayTexture();
        VideoDecoder.DisposeFrame(LastFrame);
        Present();
    }
}

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

using static FlyleafLib.Utils;

namespace FlyleafLib.Plugins
{
    public class YoutubeDL : PluginBase, IOpen, ISuggestExternalAudio, ISuggestExternalVideo
    {
        /* TODO
         * 1) Check Audio streams if we need to add also video streams with audio
         * 2) Check Best Audio bitrates/quality (mainly for audio only player)
         * 3) Dispose ytdl and not tag it to every item (use only format if required)
         * 4) Use playlist_index to set the default playlist item
         */

        public new int      Priority        { get; set; } = 1999;
        static string       plugin_path     = "yt-dlp.exe";
        static JsonSerializerOptions
                            jsonSettings    = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };

        FileSystemWatcher   watcher;
        string              workingDir;

        Process             proc;
        int                 procId = -1;
        object              procLocker = new();

        bool                addingItem;
        string              dynamicOptions = "";
        bool                errGenericImpersonate;
        long                sessionId = -1; // same for playlists
        int                 retries;

        static HashSet<string>
                            subsExt = new(StringComparer.OrdinalIgnoreCase) { "vtt", "srt", "ass", "ssa" };

        public override Dictionary<string, string> GetDefaultOptions()
            => new()
            {
                { "ExtraArguments", "" }, // TBR: Restore default functionality with --cookies-from-browser {defaultBrowser} || https://github.com/yt-dlp/yt-dlp/issues/7271
                { "MaxVideoHeight", "720" },
                { "PreferVideoWithAudio", "False" },
            };

        public override void OnInitializing()
            => DisposeInternal();

        public override void Dispose()
            => DisposeInternal();

        private Format GetAudioOnly(YoutubeDLJson ytdl)
        {
            // Prefer best with no video and protocol
            // Prioritize m3u8 protocol because https is very slow on YouTube
            var m3u8Formats = ytdl.formats.Where(f => f.protocol == "m3u8_native").ToList();
            for (int i = m3u8Formats.Count - 1; i >= 0; i--)
                if (HasAudio(m3u8Formats[i]) && !HasVideo(m3u8Formats[i]))
                    return m3u8Formats[i];

            // Prefer best with no video (dont waste bandwidth)
            for (int i = ytdl.formats.Count - 1; i >= 0; i--)
                if (HasAudio(ytdl.formats[i]) && !HasVideo(ytdl.formats[i]))
                    return ytdl.formats[i];

            // Prefer audio from worst video?
            for (int i = 0; i < ytdl.formats.Count; i++)
                if (HasAudio(ytdl.formats[i]))
                    return ytdl.formats[i];

            return null;
        }
        private Format GetBestMatch(YoutubeDLJson ytdl)
        {
            // TODO: Expose in settings (vCodecs Blacklist) || Create a HW decoding failed list dynamic (check also for whitelist)
            List<string> vCodecsBlacklist = [];

            int maxHeight;

            if (int.TryParse(Options["MaxVideoHeight"], out var height) && height > 0)
                maxHeight = Math.Min(Config.Video.MaxVerticalResolution, height);
            else
                maxHeight = Config.Video.MaxVerticalResolution;

            // Video Streams Order based on Screen Resolution
            var iresults =
                from    format in ytdl.formats
                where   HasVideo(format) && format.height <= maxHeight && (!Regex.IsMatch(format.protocol, "dash", RegexOptions.IgnoreCase) || format.vcodec.ToLower() == "vp9")
                orderby format.width    descending,
                        format.height   descending,
                        format.protocol descending, // prefer m3u8 over https (for performance)
                        format.vcodec   descending, // prefer vp09 over avc   (for performance)
                        format.tbr      descending,
                        format.fps      descending
                select  format;

            if (iresults == null || iresults.Count() == 0)
            {
                // Fall-back to any
                iresults =
                    from    format in ytdl.formats
                    where   HasVideo(format)
                    orderby format.width    descending,
                            format.height   descending,
                            format.protocol descending,
                            format.vcodec   descending,
                            format.tbr      descending,
                            format.fps      descending
                    select  format;

                if (iresults == null || iresults.Count() == 0) return null;
            }

            List<Format> results = iresults.ToList();

            // Best Resolution
            double bestWidth = results[0].width;
            double bestHeight = results[0].height;

            // Choose from the best resolution (0. with acodec and not blacklisted 1. not blacklisted 2. any)
            int priority = 1;
            if (bool.TryParse(Options["PreferVideoWithAudio"], out var v) && v)
            {
                priority = 0;
            }
            while (priority < 3)
            {
                for (int i = 0; i < results.Count; i++)
                {
                    if (results[i].width != bestWidth || results[i].height != bestHeight)
                        break;

                    if (priority == 0 && !IsBlackListed(vCodecsBlacklist, results[i].vcodec) && results[i].acodec != "none")
                        return results[i];
                    else if (priority == 1 && !IsBlackListed(vCodecsBlacklist, results[i].vcodec))
                        return results[i];
                    else if (priority == 2)
                        return results[i];
                }

                priority++;
            }

            return results[results.Count - 1]; // Fall-back to any
        }
        private static bool IsBlackListed(List<string> blacklist, string codec)
        {
            foreach (string codec2 in blacklist)
                if (Regex.IsMatch(codec, codec2, RegexOptions.IgnoreCase))
                    return true;

            return false;
        }
        private static bool HasVideo(Format fmt)
        {
            if (fmt.height > 0 || fmt.vbr > 0 || fmt.vcodec != "none")
                return true;

            return false;
        }
        private static bool HasAudio(Format fmt)
        {
            if (fmt.abr > 0 || fmt.acodec != "none")
                return true;

            return false;
        }

        private static bool IsAutomaticSubtitle(string url)
        {
            if (url.Contains("youtube") && url.Contains("/api/timedtext"))
                return true;

            return false;
        }

        private void DisposeInternal()
        {
            lock (procLocker)
            {
                if (Disposed)
                    return;

                Log.Debug($"Disposing ({procId})");

                if (procId != -1)
                {
                    Process.Start(new ProcessStartInfo
                    {
                        FileName        = "taskkill",
                        Arguments       = $"/pid {procId} /f /t",
                        CreateNoWindow  = true,
                        UseShellExecute = false,
                        WindowStyle     = ProcessWindowStyle.Hidden,
                    }).WaitForExit();
                }

                retries         =  0;
                sessionId       = -1;
                dynamicOptions  = "";
                errGenericImpersonate = false;

                if (watcher != null)
                {
                    watcher.Dispose();
                    watcher = null;
                }

                if (workingDir != null)
                {
                    Log.Debug($"Folder deleted ({workingDir})");
                    Directory.Delete(workingDir, true);
                    workingDir = null;
                }

                Disposed = true;
                Log.Debug($"Disposed ({procId})");
            }
        }

        private void NewPlaylistItem(string path)
        {
            string json = null;

            // File Watcher informs us on rename but the process still accessing the file
            for (int i=0; i<3; i++)
            {
                Thread.Sleep(20);
                try { json = File.ReadAllText(path); } catch { if (sessionId != Handler.OpenCounter) return; continue; }
                break;
            }

            YoutubeDLJson ytdl = null;

            try
            {
                ytdl = JsonSerializer.Deserialize<YoutubeDLJson>(json, jsonSettings);
            } catch (Exception e)
            {
                Log.Error($"[JsonSerializer] {e.Message}");
            }

            if (sessionId != Handler.OpenCounter) return;

            if (ytdl == null)
                return;

            if (ytdl._type == "playlist")
                return;

            PlaylistItem item = new();

            if (Playlist.ExpectingItems == 0)
                Playlist.ExpectingItems = (int)ytdl.playlist_count;

            if (Playlist.Title == null)
            {
                if (!string.IsNullOrEmpty(ytdl.playlist_title))
                {
                    Playlist.Title = ytdl.playlist_title;
                    Log.Debug($"Playlist Title -> {Playlist.Title}");
                }
                else if (!string.IsNullOrEmpty(ytdl.playlist))
                {
                    Playlist.Title = ytdl.playlist;
                    Log.Debug($"Playlist Title -> {Playlist.Title}");
                }
            }

            item.Title = ytdl.title;
            Log.Debug($"Adding {item.Title}");

            item.DirectUrl = ytdl.webpage_url;

            if (ytdl.chapters != null && ytdl.chapters.Count > 0)
            {
                item.Chapters.AddRange(ytdl.chapters.Select(c => new Demuxer.Chapter()
                {
                    StartTime = TimeSpan.FromSeconds(c.start_time).Ticks,
                    EndTime = TimeSpan.FromSeconds(c.end_time).Ticks,
                    Title = c.title
                }));
            }

            // If no formats still could have a single format attched to the main root class
            if (ytdl.formats == null)
                ytdl.formats = [ytdl];

            // Audio / Video Streams
            for (int i=0; i<ytdl.formats.Count; i++)
            {
                if (sessionId != Handler.OpenCounter)
                    return;

                Format fmt = ytdl.formats[i];

                if (ytdl.formats[i].vcodec == null)
                    ytdl.formats[i].vcodec = "";

                if (ytdl.formats[i].acodec == null)
                    ytdl.formats[i].acodec = "";

                if (ytdl.formats[i].protocol == null)
                    ytdl.formats[i].protocol = "";

                bool hasAudio = HasAudio(fmt);
                bool hasVideo = HasVideo(fmt);

                if (!hasVideo && !hasAudio)
                    continue;

                ExternalStream extStream;

                if (hasVideo)
                {
                    extStream = new ExternalVideoStream()
                    {
                        Url         = fmt.url,
                        UrlFallback = string.IsNullOrEmpty(fmt.manifest_url) ? ytdl.manifest_url : fmt.manifest_url,
                        Protocol    = fmt.protocol,
                        HasAudio    = hasAudio,
                        BitRate     = (long)fmt.vbr,
                        Codec       = fmt.vcodec,
                        //Language = Language.Get(fmt.language),
                        Width       = (int)fmt.width,
                        Height      = (int)fmt.height,
                        FPS         = fmt.fps
                    };
                }
                else
                {
                    extStream = new ExternalAudioStream()
                    {
                        Url         = fmt.url,
                        UrlFallback = string.IsNullOrEmpty(fmt.manifest_url) ? ytdl.manifest_url : fmt.manifest_url,
                        Protocol    = fmt.protocol,
                        HasVideo    = hasVideo,
                        BitRate     = (long)fmt.abr,
                        Codec       = fmt.acodec,
                        Language    = Language.Get(fmt.language)
                    };
                }

                AddHeaders(extStream, fmt);
                AddExternalStream(extStream, fmt, item);
            }

            if (GetBestMatch(ytdl) == null && GetAudioOnly(ytdl) == null)
            {
                Log.Warn("No streams found");
                return;
            }

            // Subtitles Streams
            try
            {
                // May not be available
                Language videoLang = ytdl.language == null ? Language.Unknown : Language.Get(ytdl.language);
                HashSet<string> addedUrl = new(); // for de-duplication

                if (ytdl.automatic_captions != null)
                {
                    foreach (var subtitle1 in ytdl.automatic_captions)
                    {
                        if (sessionId != Handler.OpenCounter)
                            return;

                        // original (source) language has this suffix
                        const string suffix = "-orig";

                        string langCode = subtitle1.Key;
                        bool isOriginal = langCode.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);

                        if (isOriginal)
                        {
                            // remove -orig suffix
                            langCode = langCode[..^suffix.Length];
                        }

                        Language lang = Language.Get(langCode);

                        foreach (var subtitle in subtitle1.Value)
                        {
                            if (!subsExt.Contains(subtitle.ext))
                                continue;

                            bool automatic = IsAutomaticSubtitle(subtitle.url);

                            if (!isOriginal && automatic) // always load original subtitle
                            {
                                // Automatic subtitles are loaded under the following ORed conditions
                                // 1. If the language matches the set language
                                // 2. Subtitles in the same language as the video
                                if (!(Config.Subtitles.Languages.Contains(lang) || videoLang != Language.Unknown && videoLang == lang))
                                {
                                    continue;
                                }
                            }

                            // because -orig may be duplicated
                            if (!addedUrl.Add(subtitle.url))
                                continue;

                            AddExternalStream(new ExternalSubtitlesStream()
                            {
                                Downloaded    = true,
                                Protocol      = subtitle.ext,
                                Language      = lang,
                                Url           = subtitle.url,
                                Automatic     = automatic
                            }, null, item);
                        }
                    }
                }
            } catch (Exception e) { Log.Warn($"Failed to add subtitles ({e.Message})"); }

            AddPlaylistItem(item, ytdl);
        }
        public void AddHeaders(ExternalStream extStream, Format fmt)
        {
            if (fmt.http_headers != null)
            {
                if (fmt.http_headers.TryGetValue("User-Agent", out string value))
                {
                    extStream.UserAgent = value;
                    fmt.http_headers.Remove("User-Agent");
                }

                if (fmt.http_headers.TryGetValue("Referer", out value))
                {
                    extStream.Referrer = value;
                    fmt.http_headers.Remove("Referer");
                }

                extStream.HTTPHeaders = fmt.http_headers;

                if (!string.IsNullOrEmpty(fmt.cookies))
                    extStream.HTTPHeaders.Add("Cookies", fmt.cookies);

            }
        }

        public bool CanOpen()
        {
            try
            {
                if (Playlist.IOStream != null)
                    return false;

                Uri uri = new(Playlist.Url);
                string scheme = uri.Scheme.ToLower();

                if (scheme != "http" && scheme != "https")
                    return false;

                string ext = GetUrlExtention(uri.AbsolutePath);

                if (ext == "m3u8" || ext == "mp3" || ext == "m3u" || ext == "pls")
                    return false;

                // TBR: try to avoid processing radio stations
                if (string.IsNullOrEmpty(uri.PathAndQuery) || uri.PathAndQuery.Length < 5)
                    return false;

            } catch (Exception) { return false; }

            return true;
        }
        public OpenResults Open()
        {
            try
            {
                lock (procLocker)
                {
                    Disposed = false;
                    sessionId = Handler.OpenCounter;
                    Playlist.InputType = InputType.Web;

                    workingDir = Path.GetTempPath() + Guid.NewGuid().ToString();

                    Log.Debug($"Folder created ({workingDir})");
                    Directory.CreateDirectory(workingDir);
                    proc = new Process
                    {
                        EnableRaisingEvents = true,

                        StartInfo = new ProcessStartInfo
                        {
                            FileName        = Path.Combine(Engine.Plugins.Folder, Name, plugin_path),
                            Arguments       = $"{dynamicOptions}{Options["ExtraArguments"]} --no-check-certificate --skip-download --youtube-skip-dash-manifest --write-info-json -P \"{workingDir}\" \"{Playlist.Url}\" -o \"%(title).220B\"", // 418 max filename length
                            CreateNoWindow  = true,
                            UseShellExecute = false,
                            WindowStyle     = ProcessWindowStyle.Hidden,
                            RedirectStandardError   = true,
                            RedirectStandardOutput  = Logger.CanDebug,
                        }
                    };

                    proc.Exited += (o, e) =>
                    {
                        lock (procLocker)
                        {
                            if (Logger.CanDebug)
                                Log.Debug($"Process completed ({(procId == -1 ? "Killed" : $"{procId}")})");

                            proc.Close();
                            proc    = null;
                            procId  = -1;
                        }
                    };

                    proc.ErrorDataReceived += (o, e) =>
                    {
                        if (sessionId != Handler.OpenCounter || e.Data == null)
                            return;

                        Log.Debug($"[stderr] {e.Data}");

                        if (!errGenericImpersonate && e.Data.Contains("generic:impersonate"))
                            errGenericImpersonate = true;
                    };

                    if (Logger.CanDebug)
                        proc.OutputDataReceived += (o, e) =>
                        {
                            if (sessionId == Handler.OpenCounter)
                                Log.Debug($"[stdout] {e.Data}");
                        };

                    watcher = new()
                    {
                        Path = workingDir,
                        EnableRaisingEvents = true,
                    };
                    watcher.Renamed += (o, e) =>
                    {
                        try
                        {
                            if (sessionId != Handler.OpenCounter)
                                return;

                            addingItem = true;

                            NewPlaylistItem(e.FullPath);

                            if (Playlist.Items.Count == 1)
                                Handler.OnPlaylistCompleted();

                        } catch (Exception e2) { Log.Warn($"Renamed Event Error {e2.Message} | {sessionId != Handler.OpenCounter}");
                        } finally { addingItem = false; }
                    };

                    proc.Start();
                    procId = proc.Id;
                    Log.Debug($"Process started ({procId})");

                    // Don't try to read them at once at the end as the buffers (hardcoded to 4096) can be full and proc will freeze
                    proc.BeginErrorReadLine();
                    if (Logger.CanDebug)
                        proc.BeginOutputReadLine();
                }

                while (Playlist.Items.Count < 1 && (proc != null || addingItem) && sessionId == Handler.OpenCounter)
                    Thread.Sleep(35);

                if (sessionId != Handler.OpenCounter)
                {
                    Log.Info("Session cancelled");
                    DisposeInternal();
                    return null;
                }

                if (Playlist.Items.Count == 0) // Allow fallback to default plugin in case of YT-DLP bug with windows filename (this affects proper direct URLs as well)
                {
                    if (!errGenericImpersonate || retries > 0)
                        return null;

                    Log.Warn("Re-trying with --extractor-args \"generic:impersonate\"");
                    DisposeInternal();
                    retries = 1;
                    dynamicOptions = "--extractor-args \"generic:impersonate\" ";
                    return Open();
                }
            }
            catch (Exception e) { Log.Error($"Open ({e.Message})"); return new(e.Message); }

            return new();
        }

        public OpenResults OpenItem()
            => new();

        public ExternalAudioStream SuggestExternalAudio()
        {
            if (Handler.OpenedPlugin == null || Handler.OpenedPlugin.Name != Name)
                return null;

            var fmt = GetAudioOnly((YoutubeDLJson)GetTag(Selected));
            if (fmt == null)
                return null;

            foreach (var extStream in Selected.ExternalAudioStreams)
                if (fmt.url == extStream.Url)
                    return extStream;

            return null;
        }
        public ExternalVideoStream SuggestExternalVideo()
        {
            if (Handler.OpenedPlugin == null || Handler.OpenedPlugin.Name != Name)
                return null;

            Format fmt = GetBestMatch((YoutubeDLJson)GetTag(Selected));
            if (fmt == null)
                return null;

            foreach (var extStream in Selected.ExternalVideoStreams)
                if (fmt.url == extStream.Url)
                    return extStream;

            return null;
        }
    }
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using FlyleafLib;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using Microsoft.Win32;
using static FlyleafLib.MediaFramework.MediaContext.DecoderContext;

namespace LLPlayer.ViewModels;

public class SubtitlesDownloaderDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }
    private readonly OpenSubtitlesProvider _subProvider;

    public SubtitlesDownloaderDialogVM(
        FlyleafManager fl,
        OpenSubtitlesProvider subProvider
        )
    {
        FL = fl;
        _subProvider = subProvider;
    }

    public ObservableCollection<SearchResponse> Subs { get; } = new();

    public SearchResponse? SelectedSub
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanAction));
            }
        }
    } = null;

    public string Query
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanSearch));
            }
        }
    } = string.Empty;

    public bool CanSearch => !string.IsNullOrWhiteSpace(Query);
    public bool CanAction => SelectedSub != null;

    public AsyncDelegateCommand? CmdSearch => field ??= new AsyncDelegateCommand(async () =>
    {
        Subs.Clear();

        IList<SearchResponse> result;

        try
        {
            result = await _subProvider.Search(Query);
        }
        catch (Exception ex)
        {
            ErrorDialogHelper.ShowUnknownErrorPopup($"Cannot search subtitles from opensubtitles.org: {ex.Message}", UnknownErrorType.Network, ex);
            return;
        }

        var query = result
            .OrderByDescending(r =>
            {
                // prefer user-configured languages
                return FL.PlayerConfig.Subtitles.Languages.Any(l =>
                    l.Equals(Language.Get(r.ISO639)));
            })
            .ThenBy(r => r.LanguageName)
            .ThenByDescending(r => r.SubDownloadsCnt)
            .ThenBy(r => r.SubFileName);

        foreach (var record in query)
        {
            Subs.Add(record);
        }
    }).ObservesCanExecute(() => CanSearch);

    public AsyncDelegateCommand? CmdLoad => field ??= new AsyncDelegateCommand(async () =>
    {
        var sub = SelectedSub;
        if (sub == null)
        {
            return;
        }

        byte[] subData;

        try
        {
            (subData, _) = await _subProvider.Download(sub);
        }
        catch (Exception ex)
        {
            ErrorDialogHelper.ShowUnknownErrorPopup($"Cannot load the subtitle from opensubtitles.org: {ex.Message}", UnknownErrorType.Network, ex);
            return;
        }

        string subDir = Path.Combine(Path.GetTempPath(), App.Name, "Subs");
        string subPath = Path.Combine(subDir, sub.SubFileName);
        if (!Directory.Exists(subDir))
        {
            Directory.CreateDirectory(subDir);
        }

        var ext = Path.GetExtension(subPath).ToLower();
        if (ext.StartsWith("."))
        {
            ext = ext.Substring(1);
        }

        if (!Utils.ExtensionsSubtitles.Contains(ext))
        {
            throw new InvalidOperationException($"'{ext}' extension is not supported");
        }

        await File.WriteAllBytesAsync(subPath, subData);

        // TODO: L: Refactor to pass language directly at Open
        // TODO: L: Allow to load as a secondary subtitle
        FL.Player.decoder.OpenExternalSubtitlesStreamCompleted += DecoderOnOpenExternalSubtitlesStreamCompleted;

        void DecoderOnOpenExternalSubtitlesStreamCompleted(object? sender, OpenExternalSubtitlesStreamCompletedArgs e)
        {
            FL.Player.decoder.OpenExternalSubtitlesStreamCompleted -= DecoderOnOpenExternalSubtitlesStreamCompleted;

            if (e.Success)
            {
                var stream = e.ExtStream;
                if (stream != null && stream.Url == subPath)
                {
                    // Override if different from auto-detected language
                    stream.ManualDownloaded = true;

                    if (stream.Language.ISO6391 != sub.ISO639)
                    {
                        var lang = Language.Get(sub.ISO639);
                        if (!string.IsNullOrEmpty(lang.IdSubLanguage) && lang.IdSubLanguage != "und")
                        {
                            stream.Language = lang;
                            FL.Player.SubtitlesManager[0].LanguageSource = stream.Language;
                        }
                    }
                }
            }
        }

        FL.Player.OpenAsync(subPath);

    }).ObservesCanExecute(() => CanAction);

    public AsyncDelegateCommand? CmdDownload => field ??= new AsyncDelegateCommand(async () =>
    {
        var sub = SelectedSub;
        if (sub == null)
        {
            return;
        }

        string fileName = sub.SubFileName;

        string? initDir = null;
        if (FL.Player.Playlist.Selected != null)
        {
            var url = FL.Player.Playlist.Selected.DirectUrl;
            if (File.Exists(url))
            {
                initDir = Path.GetDirectoryName(url);
            }
        }

        SaveFileDialog dialog = new()
        {
            Title = "Save subtitles to file",
            InitialDirectory = initDir,
            FileName = fileName,
            Filter = "All Files (*.*)|*.*",
        };

        if (dialog.ShowDialog() == true)
        {
            byte[] subData;

            try
            {
                (subData, _) = await _subProvider.Download(sub);
            }
            catch (Exception ex)
            {
                ErrorDialogHelper.ShowUnknownErrorPopup($"Cannot download the subtitle from opensubtitles.org: {ex.Message}", UnknownErrorType.Network, ex);
                return;
            }

            await File.WriteAllBytesAsync(dialog.FileName, subData);
        }
    }).ObservesCanExecute(() => CanAction);

    private void Playlist_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(FL.Player.Playlist.Selected) &&
            FL.Player.Playlist.Selected != null)
        {
            // Update query when video changes
            UpdateQuery(FL.Player.Playlist.Selected);
        }
    }

    private void UpdateQuery(PlaylistItem selected)
    {
        string title = selected.Title;
        if (title == selected.OriginalTitle && File.Exists(selected.Url))
        {
            FileInfo fi = new(selected.Url);
            if (!string.IsNullOrEmpty(fi.Extension) && title.EndsWith(fi.Extension))
            {
                // remove extension part
                title = title[..^fi.Extension.Length];
            }
        }

        Query = title;
    }

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); }
        = $"Subtitles Downloader - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 900;
    public double WindowHeight { get; set => Set(ref field, value); } = 600;

    public DialogCloseListener RequestClose { get; }

    public bool CanCloseDialog()
    {
        return true;
    }
    public void OnDialogClosed()
    {
        FL.Player.Playlist.PropertyChanged -= Playlist_OnPropertyChanged;
    }
    public void OnDialogOpened(IDialogParameters parameters)
    {
        // Set query from current video
        var selected = FL.Player.Playlist.Selected;
        if (selected != null)
        {
            UpdateQuery(selected);
        }

        // Register update playlist event
        FL.Player.Playlist.PropertyChanged += Playlist_OnPropertyChanged;
    }
    #endregion
}

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;

using FlyleafLib.MediaFramework.MediaProgram;
using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaPlayer;
using static FlyleafLib.Config;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDemuxer;

public unsafe class Demuxer : RunThreadBase
{
    /* TODO
     * 1) Review lockFmtCtx on Enable/Disable Streams causes delay and is not fully required
     * 2) Include AV_DISPOSITION_ATTACHED_PIC images in streams as VideoStream with flag
     *      Possible introduce ImageStream and include also video streams with < 1 sec duration (eg. mjpeg etc)
     */

    #region Properties
    public MediaType                Type            { get; private set; }
    public DemuxerConfig            Config          { get; set; }

    // Format Info
    public string                   Url             { get; private set; }
    public string                   Name            { get; private set; }
    public string                   LongName        { get; private set; }
    public string                   Extensions      { get; private set; }
    public string                   Extension       { get; private set; }
    public long                     StartTime       { get; private set; }
    public long                     StartRealTime   { get; private set; }
    public long                     Duration        { get; internal set; }
    public void ForceDuration(long duration) { Duration = duration; IsLive = duration != 0; }

    public Dictionary<string, string>
                                    Metadata        { get; internal set; } = new Dictionary<string, string>();

    /// <summary>
    /// The time of first packet in the queue (zero based, substracts start time)
    /// </summary>
    public long                     CurTime         => CurPackets.CurTime != 0 ? CurPackets.CurTime : lastSeekTime;

    /// <summary>
    /// The buffered time in the queue (last packet time - first packet time)
    /// </summary>
    public long                     BufferedDuration=> CurPackets.BufferedDuration;

    public bool                     IsLive          { get; private set; }
    public bool                     IsHLSLive       { get; private set; }

    public AVFormatContext*         FormatContext   => fmtCtx;
    public CustomIOContext          CustomIOContext { get; private set; }

    // Media Programs
    public ObservableCollection<Program>
                                    Programs        { get; private set; } = new ObservableCollection<Program>();

    // Media Streams
    public ObservableCollection<AudioStream>
                                    AudioStreams    { get; private set; } = new ObservableCollection<AudioStream>();
    public ObservableCollection<VideoStream>
                                    VideoStreams    { get; private set; } = new ObservableCollection<VideoStream>();
    public ObservableCollection<SubtitlesStream>
                                    SubtitlesStreamsAll
                                                    { get; private set; } = new ObservableCollection<SubtitlesStream>();
    public ObservableCollection<DataStream>
                                    DataStreams     { get; private set; } = new ObservableCollection<DataStream>();
    readonly object lockStreams = new();

    public List<int>                EnabledStreams  { get; private set; } = new List<int>();
    public Dictionary<int, StreamBase>
                                    AVStreamToStream{ get; private set; }

    public AudioStream              AudioStream     { get; private set; }
    public VideoStream              VideoStream     { get; private set; }
    // In the case of the secondary external subtitle, there is only one stream, but it goes into index=1 and index = 0 is null.
    public SubtitlesStream[]        SubtitlesStreams
                                                    { get; private set; }
    public DataStream               DataStream      { get; private set; }

    // Audio/Video Stream's HLSPlaylist
    internal playlist*              HLSPlaylist     { get; private set; }

    // Media Packets
    public PacketQueue              Packets         { get; private set; }
    public PacketQueue              AudioPackets    { get; private set; }
    public PacketQueue              VideoPackets    { get; private set; }
    public PacketQueue[]            SubtitlesPackets
                                                    { get; private set; }
    public PacketQueue              DataPackets     { get; private set; }
    public PacketQueue              CurPackets      { get; private set; }

    public bool                     UseAVSPackets   { get; private set; }

    public PacketQueue GetPacketsPtr(StreamBase stream)
    {
        if (!UseAVSPackets)
        {
            return Packets;
        }

        switch (stream.Type)
        {
            case MediaType.Audio:
                return AudioPackets;
            case MediaType.Video:
                return VideoPackets;
            case MediaType.Subs:
                return SubtitlesPackets[SubtitlesSelectedHelper.CurIndex];
            default:
                return DataPackets;
        }
    }

    public ConcurrentQueue<ConcurrentStack<List<IntPtr>>>
                                    VideoPacketsReverse
                                                    { get; private set; } = new ConcurrentQueue<ConcurrentStack<List<IntPtr>>>();

    public bool                     IsReversePlayback
                                                    { get; private set; }

    public long                     TotalBytes      { get; private set; } = 0;

    // Interrupt
    public Interrupter              Interrupter     { get; private set; }

    public ObservableCollection<Chapter>
                                    Chapters        { get; private set; } = new ObservableCollection<Chapter>();
    public class Chapter
    {
        public long     StartTime   { get; set; }
        public long     EndTime     { get; set; }
        public string   Title       { get; set; }
    }

    public event EventHandler AudioLimit;
    bool audioBufferLimitFired;
    void OnAudioLimit()
        => Task.Run(() => AudioLimit?.Invoke(this, new EventArgs()));

    public event EventHandler TimedOut;
    internal void OnTimedOut()
        => Task.Run(() => TimedOut?.Invoke(this, new EventArgs()));
    #endregion

    #region Constructor / Declaration
    public AVPacket*        packet;
    public AVFormatContext* fmtCtx;
    internal HLSContext*    hlsCtx;

    long                    hlsPrevSeqNo            = AV_NOPTS_VALUE;   // Identifies the change of the m3u8 playlist (wraped)
    internal long           hlsStartTime            = AV_NOPTS_VALUE;   // Calculation of first timestamp (lastPacketTs - hlsCurDuration)
    long                    hlsCurDuration;                             // Duration until the start of the current segment
    long                    lastSeekTime;                               // To set CurTime while no packets are available

    public object           lockFmtCtx              = new();
    internal bool           allowReadInterrupts;

    /* Reverse Playback
     *
     * Video Packets Queue (FIFO)                       ConcurrentQueue<ConcurrentStack<List<IntPtr>>>
     *      Video Packets Seek Stacks (FILO)            ConcurrentStack<List<IntPtr>>
     *          Video Packets List Keyframe (List)      List<IntPtr>
     */

    long                    curReverseStopPts       = AV_NOPTS_VALUE;
    long                    curReverseStopRequestedPts
                                                    = AV_NOPTS_VALUE;
    long                    curReverseStartPts      = AV_NOPTS_VALUE;
    List<IntPtr>            curReverseVideoPackets  = new();
    ConcurrentStack<List<IntPtr>>
                            curReverseVideoStack    = new();
    long                    curReverseSeekOffset;

    // Required for passing AV Options and HTTP Query params to the underlying contexts
    AVFormatContext_io_open ioopen;
    AVFormatContext_io_open ioopenDefault;
    AVDictionary*           avoptCopy;
    Dictionary<string, string>
                            queryParams;
    byte[]                  queryCachedBytes;

    // for TEXT subtitles buffer
    byte*                   inputData;
    int                     inputDataSize;
    internal AVIOContext*   avioCtx;

    int subNum => Config.config.Subtitles.Max;

    public Demuxer(DemuxerConfig config, MediaType type = MediaType.Video, int uniqueId = -1, bool useAVSPackets = true) : base(uniqueId)
    {
        Config          = config;
        Type            = type;
        UseAVSPackets   = useAVSPackets;
        Interrupter     = new Interrupter(this);
        CustomIOContext = new CustomIOContext(this);

        Packets         = new PacketQueue(this);
        AudioPackets    = new PacketQueue(this);
        VideoPackets    = new PacketQueue(this);
        SubtitlesPackets = new PacketQueue[subNum];
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesPackets[i] = new PacketQueue(this);
        }
        SubtitlesStreams = new SubtitlesStream[subNum];

        DataPackets     = new PacketQueue(this);
        CurPackets      = Packets; // Will be updated on stream switch in case of AVS

        string typeStr = Type == MediaType.Video ? "Main" : Type.ToString();
        threadName = $"Demuxer: {typeStr,5}";

        Utils.UIInvokeIfRequired(() =>
        {
            BindingOperations.EnableCollectionSynchronization(Programs,         lockStreams);
            BindingOperations.EnableCollectionSynchronization(AudioStreams,     lockStreams);
            BindingOperations.EnableCollectionSynchronization(VideoStreams,     lockStreams);
            BindingOperations.EnableCollectionSynchronization(SubtitlesStreamsAll, lockStreams);
            BindingOperations.EnableCollectionSynchronization(DataStreams,      lockStreams);

            BindingOperations.EnableCollectionSynchronization(Chapters,         lockStreams);
        });

        ioopen = IOOpen;
    }
    #endregion

    #region Dispose / Dispose Packets
    public void DisposePackets()
    {
        if (UseAVSPackets)
        {
            AudioPackets.Clear();
            VideoPackets.Clear();
            for (int i = 0; i < subNum; i++)
            {
                SubtitlesPackets[i].Clear();
            }
            DataPackets.Clear();

            DisposePacketsReverse();
        }
        else
            Packets.Clear();

        hlsStartTime = AV_NOPTS_VALUE;
    }

    public void DisposePacketsReverse()
    {
        while (!VideoPacketsReverse.IsEmpty)
        {
            VideoPacketsReverse.TryDequeue(out var t1);
            while (!t1.IsEmpty)
            {
                t1.TryPop(out var t2);
                for (int i = 0; i < t2.Count; i++)
                {
                    if (t2[i] == IntPtr.Zero) continue;
                    AVPacket* packet = (AVPacket*)t2[i];
                    av_packet_free(&packet);
                }
            }
        }

        while (!curReverseVideoStack.IsEmpty)
        {
            curReverseVideoStack.TryPop(out var t2);
            for (int i = 0; i < t2.Count; i++)
            {
                if (t2[i] == IntPtr.Zero) continue;
                AVPacket* packet = (AVPacket*)t2[i];
                av_packet_free(&packet);
            }
        }
    }
    public void Dispose()
    {
        if (Disposed)
            return;

        lock (lockActions)
        {
            if (Disposed)
                return;

            Stop();

            Url = null;
            hlsCtx = null;

            IsReversePlayback   = false;
            curReverseStopPts   = AV_NOPTS_VALUE;
            curReverseStartPts  = AV_NOPTS_VALUE;
            hlsPrevSeqNo        = AV_NOPTS_VALUE;
            lastSeekTime        = 0;

            // Free Streams
            lock (lockStreams)
            {
                AudioStreams.Clear();
                VideoStreams.Clear();
                SubtitlesStreamsAll.Clear();
                DataStreams.Clear();
                Programs.Clear();

                Chapters.Clear();
            }
            EnabledStreams.Clear();
            AudioStream         = null;
            VideoStream         = null;
            for (int i = 0; i < subNum; i++)
            {
                SubtitlesStreams[i] = null;
            }
            DataStream          = null;
            queryParams         = null;
            queryCachedBytes    = null;

            DisposePackets();

            if (fmtCtx != null)
            {
                Interrupter.CloseRequest();
                fixed (AVFormatContext** ptr = &fmtCtx) { avformat_close_input(ptr); fmtCtx = null; }
            }

            if (avoptCopy != null) fixed (AVDictionary** ptr = &avoptCopy) av_dict_free(ptr);
            if (packet != null) fixed (AVPacket** ptr = &packet) av_packet_free(ptr);

            CustomIOContext.Dispose();

            if (avioCtx != null)
            {
                av_free(avioCtx->buffer);
                fixed (AVIOContext** ptr = &avioCtx)
                {
                    avio_context_free(ptr);
                }

                inputData = null;
                inputDataSize = 0;
            }

            TotalBytes = 0;
            Status = Status.Stopped;
            Disposed = true;

            Log.Info("Disposed");
        }
    }
    #endregion

    #region Open / Seek / Run
    public string Open(string url)      => Open(url, null);
    public string Open(Stream stream)   => Open(null, stream);
    public string Open(string url, Stream stream)
    {
        bool    gotLockActions  = false;
        bool    gotLockFmtCtx   = false;
        string  error           = null;

        try
        {
            Monitor.Enter(lockActions,ref gotLockActions);
            Dispose();
            Monitor.Enter(lockFmtCtx, ref gotLockFmtCtx);
            Url = url;

            if (string.IsNullOrEmpty(url) && stream == null)
                return "Invalid empty/null input";

            Dictionary<string, string>
                            fmtOptExtra = null;
            AVInputFormat*  inFmt       = null;
            int             ret         = -1;

            Disposed = false;
            Status   = Status.Opening;

            // Allocate / Prepare Format Context
            fmtCtx = avformat_alloc_context();
            if (Config.AllowInterrupts)
                fmtCtx->interrupt_callback.callback = Interrupter.interruptClbk;

            fmtCtx->flags |= (FmtFlags2)Config.FormatFlags;

            // Force Format (url as input and Config.FormatOpt for options)
            if (Config.ForceFormat != null)
            {
                inFmt = av_find_input_format(Config.ForceFormat);
                if (inFmt == null)
                    return error = $"[av_find_input_format] {Config.ForceFormat} not found";
            }

            // Force Custom IO Stream Context (url should be null)
            if (stream != null)
            {
                CustomIOContext.Initialize(stream);
                stream.Seek(0, SeekOrigin.Begin);
                url = null;
            }

            /* Force Format with Url syntax to support format, url and options within the input url
                *
                * fmt://$format$[/]?$input$&$options$
                *
                * deprecate support for device://
                *
                * Examples:
                *  See: https://ffmpeg.org/ffmpeg-devices.html for devices formats and options
                *
                * 1. fmt://gdigrab?title=Command Prompt&framerate=2
                * 2. fmt://gdigrab?desktop
                * 3. fmt://dshow?audio=Microphone (Relatek):video=Lenovo Camera
                * 4. fmt://rawvideo?C:\root\dev\Flyleaf\VideoSamples\rawfile.raw&pixel_format=uyvy422&video_size=1920x1080&framerate=60
                *
                */
            else if (url.StartsWith("fmt://") || url.StartsWith("device://"))
            {
                string  urlFromUrl  = null;
                string  fmtStr      = "";
                int     fmtStarts   = url.IndexOf('/') + 2;
                int     queryStarts = url.IndexOf('?');

                if (queryStarts == -1)
                    fmtStr = url[fmtStarts..];
                else
                {
                    fmtStr = url[fmtStarts..queryStarts];

                    string  query       = url[(queryStarts + 1)..];
                    int     inputEnds   = query.IndexOf('&');

                    if (inputEnds == -1)
                        urlFromUrl  = query;
                    else
                    {
                        urlFromUrl  = query[..inputEnds];
                        query       = query[(inputEnds + 1)..];

                        fmtOptExtra = Utils.ParseQueryString(query);
                    }
                }

                url     = urlFromUrl;
                fmtStr  = fmtStr.Replace("/", "");
                inFmt   = av_find_input_format(fmtStr);
                if (inFmt == null)
                    return error = $"[av_find_input_format] {fmtStr} not found";
            }
            else if (url.StartsWith("srt://"))
            {
                ReadOnlySpan<char> urlSpan = url.AsSpan();
                int queryPos = urlSpan.IndexOf('?');

                if (queryPos != -1)
                {
                    fmtOptExtra = Utils.ParseQueryString(urlSpan.Slice(queryPos + 1));
                    url = urlSpan[..queryPos].ToString();
                }
            }

            if (Config.FormatOptToUnderlying && url != null && (url.StartsWith("http://") || url.StartsWith("https://")))
            {
                queryParams = [];
                if (Config.DefaultHTTPQueryToUnderlying)
                {
                    int queryStarts = url.IndexOf('?');
                    if (queryStarts != -1)
                    {
                        var qp = Utils.ParseQueryString(url.AsSpan()[(queryStarts + 1)..]);
                        foreach (var kv in qp)
                            queryParams[kv.Key] = kv.Value;
                    }
                }

                foreach (var kv in Config.ExtraHTTPQueryParamsToUnderlying)
                    queryParams[kv.Key] = kv.Value;

                if (queryParams.Count > 0)
                {
                    var queryCachedStr = "?";
                    foreach (var kv in queryParams)
                        queryCachedStr += kv.Value == null ? $"{kv.Key}&" : $"{kv.Key}={kv.Value}&";

                    queryCachedStr = queryCachedStr[..^1];
                    queryCachedBytes = Encoding.UTF8.GetBytes(queryCachedStr);
                }
                else
                    queryParams = null;
            }

            if (Type == MediaType.Subs &&
                Utils.ExtensionsSubtitlesText.Contains(Utils.GetUrlExtention(url)) &&
                File.Exists(url))
            {
                // If the files can be read with text subtitles, load them all into memory and convert them to UTF8.
                // Because ffmpeg expects UTF8 text.
                try
                {
                    FileInfo file = new(url);
                    if (file.Length >= 10 * 1024 * 1024)
                    {
                        throw new InvalidOperationException($"TEXT subtitle is too big (>=10MB) to load: {file.Length}");
                    }

                    // Detects character encoding, reads text and converts to UTF8
                    Encoding encoding = Encoding.UTF8;

                    Encoding detected = TextEncodings.DetectEncoding(url);
                    if (detected != null)
                    {
                        encoding = detected;
                    }

                    string content = File.ReadAllText(url, encoding);
                    byte[] contentBytes = Encoding.UTF8.GetBytes(content);

                    inputDataSize = contentBytes.Length;
                    inputData = (byte*)av_malloc((nuint)inputDataSize);

                    Span<byte> src = new(contentBytes);
                    Span<byte> dst = new(inputData, inputDataSize);
                    src.CopyTo(dst);

                    avioCtx = avio_alloc_context(inputData, inputDataSize, 0, null, null, null, null);
                    if (avioCtx == null)
                    {
                        throw new InvalidOperationException("avio_alloc_context");
                    }

                    // Pass to ffmpeg by on-memory
                    fmtCtx->pb = avioCtx;
                    fmtCtx->flags |= FmtFlags2.CustomIo;
                }
                catch (Exception ex)
                {
                    Log.Warn($"Could not load text subtitles to memory: {ex.Message}");
                }
            }

            // Some devices required to be opened from a UI or STA thread | after 20-40 sec. of demuxing -> [gdigrab @ 0000019affe3f2c0] Failed to capture image (error 6) or (error 8)
            bool isDevice = inFmt != null && inFmt->priv_class != null && (
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceAudioInput) ||
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceAudioOutput) ||
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceInput) ||
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceOutput) ||
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceVideoInput) ||
                    inFmt->priv_class->category.HasFlag(AVClassCategory.DeviceVideoOutput)
                    );

            // Open Format Context
            allowReadInterrupts = true; // allow Open interrupts always
            Interrupter.OpenRequest();

            // Nesting the io_open (to pass the options to the underlying formats)
            if (Config.FormatOptToUnderlying)
            {
                ioopenDefault = (AVFormatContext_io_open)Marshal.GetDelegateForFunctionPointer(fmtCtx->io_open.Pointer, typeof(AVFormatContext_io_open));
                fmtCtx->io_open = ioopen;
            }

            if (isDevice)
            {
                string fmtName = Utils.BytePtrToStringUTF8(inFmt->name);

                if (fmtName == "decklink") // avoid using UI thread for decklink (STA should be enough for CoInitialize/CoCreateInstance)
                    Utils.STAInvoke(() => OpenFormat(url, inFmt, fmtOptExtra, out ret));
                else
                    Utils.UIInvoke(() => OpenFormat(url, inFmt, fmtOptExtra, out ret));
            }
            else
                OpenFormat(url, inFmt, fmtOptExtra, out ret);

            if ((ret == AVERROR_EXIT && !Interrupter.Timedout) || Status != Status.Opening || Interrupter.ForceInterrupt == 1) { if (ret < 0) fmtCtx = null; return error = "Cancelled"; }
            if (ret < 0) { fmtCtx = null; return error = Interrupter.Timedout ? "[avformat_open_input] Timeout" : $"[avformat_open_input] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})"; }

            // Find Streams Info
            if (Config.AllowFindStreamInfo)
            {
                /* For some reason HdmvPgsSubtitle requires more analysis (even when it has already all the information)
                 *
                 * Increases delay & memory and it will not free it after analysis (fmtctx internal buffer)
                 *  - avformat_flush will release it but messes with the initial seek position (possible seek to start to force it releasing it but still we have the delay)
                 *
                 * Consider
                 *  - DVD/Blu-ray/mpegts only? (possible HLS -> mpegts?*)
                 *  - Re-open in case of "Consider increasing the value for the 'analyzeduration'" (catch from ffmpeg log)
                 *
                 *  https://github.com/SuRGeoNix/Flyleaf/issues/502
                 */

                if (Utils.BytePtrToStringUTF8(fmtCtx->iformat->name) == "mpegts")
                {
                    bool requiresMoreAnalyse = false;

                    for (int i = 0; i < fmtCtx->nb_streams; i++)
                        if (fmtCtx->streams[i]->codecpar->codec_id == AVCodecID.HdmvPgsSubtitle ||
                            fmtCtx->streams[i]->codecpar->codec_id == AVCodecID.DvdSubtitle
                            )
                            { requiresMoreAnalyse = true; break; }

                    if (requiresMoreAnalyse)
                    {
                        fmtCtx->probesize = Math.Max(fmtCtx->probesize, 5000 * (long)1024 * 1024); // Bytes
                        fmtCtx->max_analyze_duration = Math.Max(fmtCtx->max_analyze_duration, 1000 * (long)1000 * 1000); // Mcs
                    }
                }

                ret = avformat_find_stream_info(fmtCtx, null);
                if (ret == AVERROR_EXIT || Status != Status.Opening || Interrupter.ForceInterrupt == 1) return error = "Cancelled";
                if (ret < 0) return error = $"[avformat_find_stream_info] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})";
            }

            // Prevent Multiple Immediate exit requested on eof (maybe should not use avio_feof() to test for the end)
            if (fmtCtx->pb != null)
                fmtCtx->pb->eof_reached = 0;

            bool hasVideo = FillInfo();

            if (Type == MediaType.Video && !hasVideo && AudioStreams.Count == 0)
                return error = $"No audio / video stream found";
            else if (Type == MediaType.Audio && AudioStreams.Count == 0)
                return error = $"No audio stream found";
            else if (Type == MediaType.Subs && SubtitlesStreamsAll.Count == 0)
                return error = $"No subtitles stream found";

            packet = av_packet_alloc();
            Status = Status.Stopped;
            allowReadInterrupts = Config.AllowReadInterrupts && !Config.ExcludeInterruptFmts.Contains(Name);

            return error = null;
        }
        catch (Exception ex)
        {
            return error = $"Unknown: {ex.Message}";
        }
        finally
        {
            if (error != null)
                Dispose();

            if (gotLockFmtCtx)  Monitor.Exit(lockFmtCtx);
            if (gotLockActions) Monitor.Exit(lockActions);
        }
    }

    int IOOpen(AVFormatContext* s, AVIOContext** pb, byte* urlb, IOFlags flags, AVDictionary** avFmtOpts)
    {
        int ret;
        AVDictionaryEntry *t = null;

        if (avoptCopy != null)
        {
            while ((t = av_dict_get(avoptCopy, "", t, DictReadFlags.IgnoreSuffix)) != null)
                _ = av_dict_set(avFmtOpts, Utils.BytePtrToStringUTF8(t->key), Utils.BytePtrToStringUTF8(t->value), 0);
        }

        if (queryParams == null)
            ret = ioopenDefault(s, pb, urlb, flags, avFmtOpts);
        else
        {
            int urlLength   =  0;
            int queryPos    = -1;
            while (urlb[urlLength] != '\0')
            {
                if (urlb[urlLength] == '?' && queryPos == -1 && urlb[urlLength + 1] != '\0')
                    queryPos = urlLength;

                urlLength++;
            }

            // urlNoQuery + ? + queryCachedBytes
            if (queryPos == -1)
            {
                ReadOnlySpan<byte> urlNoQuery = new(urlb, urlLength);
                int         newLength   = urlLength + queryCachedBytes.Length + 1;
                Span<byte>  urlSpan     = newLength < 1024 ? stackalloc byte[newLength] : new byte[newLength];// new(urlPtr, newLength);
                urlNoQuery.CopyTo(urlSpan);
                queryCachedBytes.AsSpan().CopyTo(urlSpan[urlNoQuery.Length..]);

                fixed(byte* urlPtr =  urlSpan)
                    ret = ioopenDefault(s, pb, urlPtr, flags, avFmtOpts);
            }

            // urlNoQuery + ? + existingParams/queryParams combined
            else
            {
                ReadOnlySpan<byte> urlNoQuery   = new(urlb, queryPos);
                ReadOnlySpan<byte> urlQuery     = new(urlb + queryPos + 1, urlLength - queryPos - 1);
                var qps = Utils.ParseQueryString(Encoding.UTF8.GetString(urlQuery));

                foreach (var kv in queryParams)
                    if (!qps.ContainsKey(kv.Key))
                        qps[kv.Key] = kv.Value;

                string newQuery = "?";
                foreach (var kv in qps)
                    newQuery += kv.Value == null ? $"{kv.Key}&" : $"{kv.Key}={kv.Value}&";

                int         newLength   = urlNoQuery.Length + newQuery.Length + 1;
                Span<byte>  urlSpan     = newLength < 1024 ? stackalloc byte[newLength] : new byte[newLength];// new(urlPtr, newLength);
                urlNoQuery.CopyTo(urlSpan);
                Encoding.UTF8.GetBytes(newQuery).AsSpan().CopyTo(urlSpan[urlNoQuery.Length..]);

                fixed(byte* urlPtr =  urlSpan)
                    ret = ioopenDefault(s, pb, urlPtr, flags, avFmtOpts);
            }
        }

        return ret;
    }

    private void OpenFormat(string url, AVInputFormat* inFmt, Dictionary<string, string> opt, out int ret)
    {
        AVDictionary* avopt = null;
        var curOpt = Type == MediaType.Video ? Config.FormatOpt : (Type == MediaType.Audio ? Config.AudioFormatOpt : Config.SubtitlesFormatOpt);

        if (curOpt != null)
            foreach (var optKV in curOpt)
                av_dict_set(&avopt, optKV.Key, optKV.Value, 0);

        if (opt != null)
            foreach (var optKV in opt)
                av_dict_set(&avopt, optKV.Key, optKV.Value, 0);

        if (Config.FormatOptToUnderlying)
            fixed(AVDictionary** ptr = &avoptCopy)
                av_dict_copy(ptr, avopt, 0);

        fixed(AVFormatContext** fmtCtxPtr = &fmtCtx)
            ret = avformat_open_input(fmtCtxPtr, url, inFmt, avopt == null ? null : &avopt);

        if (avopt != null)
        {
            if (ret >= 0)
            {
                AVDictionaryEntry *t = null;

                while ((t = av_dict_get(avopt, "", t, DictReadFlags.IgnoreSuffix)) != null)
                    Log.Debug($"Ignoring format option {Utils.BytePtrToStringUTF8(t->key)}");
            }

            av_dict_free(&avopt);
        }
    }
    private bool FillInfo()
    {
        Name        = Utils.BytePtrToStringUTF8(fmtCtx->iformat->name);
        LongName    = Utils.BytePtrToStringUTF8(fmtCtx->iformat->long_name);
        Extensions  = Utils.BytePtrToStringUTF8(fmtCtx->iformat->extensions);

        StartTime       = fmtCtx->start_time != AV_NOPTS_VALUE ? fmtCtx->start_time * 10 : 0;
        StartRealTime   = fmtCtx->start_time_realtime != AV_NOPTS_VALUE ? fmtCtx->start_time_realtime * 10 : 0;
        Duration        = fmtCtx->duration > 0 ? fmtCtx->duration * 10 : 0;

        // TBR: Possible we can get Apple HTTP Live Streaming/hls with HLSPlaylist->finished with Duration != 0
        if (Engine.Config.FFmpegHLSLiveSeek && Duration == 0 && Name == "hls" && Environment.Is64BitProcess) // HLSContext cast is not safe
        {
            hlsCtx = (HLSContext*) fmtCtx->priv_data;
            StartTime = 0;
            //UpdateHLSTime(); Maybe with default 0 playlist
        }

        if (fmtCtx->nb_streams == 1 && fmtCtx->streams[0]->codecpar->codec_type == AVMediaType.Subtitle) // External Streams (mainly for .sub will have as start time the first subs timestamp)
            StartTime = 0;

        IsLive = Duration == 0 || hlsCtx != null;

        bool hasVideo = false;
        AVStreamToStream = new Dictionary<int, StreamBase>();

        Metadata.Clear();
        AVDictionaryEntry* b = null;
        while (true)
        {
            b = av_dict_get(fmtCtx->metadata, "", b, DictReadFlags.IgnoreSuffix);
            if (b == null) break;
            Metadata.Add(Utils.BytePtrToStringUTF8(b->key), Utils.BytePtrToStringUTF8(b->value));
        }

        Chapters.Clear();
        string dump = "";
        for (int i=0; i<fmtCtx->nb_chapters; i++)
        {
            var chp = fmtCtx->chapters[i];
            double tb = av_q2d(chp->time_base) * 10000.0 * 1000.0;
            string title = "";

            b = null;
            while (true)
            {
                b = av_dict_get(chp->metadata, "", b, DictReadFlags.IgnoreSuffix);
                if (b == null) break;
                if (Utils.BytePtrToStringUTF8(b->key).ToLower() == "title")
                    title = Utils.BytePtrToStringUTF8(b->value);
            }

            if (CanDebug)
                dump += $"[Chapter {i+1,-2}] {Utils.TicksToTime((long)(chp->start * tb) - StartTime)} - {Utils.TicksToTime((long)(chp->end * tb) - StartTime)} | {title}\r\n";

            Chapters.Add(new Chapter()
            {
                StartTime   = (long)((chp->start * tb) - StartTime),
                EndTime     = (long)((chp->end * tb) - StartTime),
                Title       = title
            });
        }

        if (CanDebug && dump != "") Log.Debug($"Chapters\r\n\r\n{dump}");

        bool audioHasEng = false;
        bool subsHasEng = false;

        lock (lockStreams)
        {
            for (int i=0; i<fmtCtx->nb_streams; i++)
            {
                fmtCtx->streams[i]->discard = AVDiscard.All;

                switch (fmtCtx->streams[i]->codecpar->codec_type)
                {
                    case AVMediaType.Audio:
                        AudioStreams.Add(new AudioStream(this, fmtCtx->streams[i]));
                        AVStreamToStream.Add(fmtCtx->streams[i]->index, AudioStreams[^1]);
                        audioHasEng = AudioStreams[^1].Language == Language.English;

                        break;

                    case AVMediaType.Video:
                        if ((fmtCtx->streams[i]->disposition & DispositionFlags.AttachedPic) != 0)
                            { Log.Info($"Excluding image stream #{i}"); continue; }

                        // TBR: When AllowFindStreamInfo = false we can get valid pixel format during decoding (in case of remuxing only this might crash, possible check if usedecoders?)
                        if (((AVPixelFormat)fmtCtx->streams[i]->codecpar->format) == AVPixelFormat.None && Config.AllowFindStreamInfo)
                        {
                            Log.Info($"Excluding invalid video stream #{i}");
                            continue;
                        }
                        VideoStreams.Add(new VideoStream(this, fmtCtx->streams[i]));
                        AVStreamToStream.Add(fmtCtx->streams[i]->index, VideoStreams[^1]);
                        hasVideo |= !Config.AllowFindStreamInfo || VideoStreams[^1].PixelFormat != AVPixelFormat.None;

                        break;

                    case AVMediaType.Subtitle:
                        SubtitlesStreamsAll.Add(new SubtitlesStream(this, fmtCtx->streams[i]));
                        AVStreamToStream.Add(fmtCtx->streams[i]->index, SubtitlesStreamsAll[^1]);
                        subsHasEng = SubtitlesStreamsAll[^1].Language == Language.English;
                        break;

                    case AVMediaType.Data:
                        DataStreams.Add(new DataStream(this, fmtCtx->streams[i]));
                        AVStreamToStream.Add(fmtCtx->streams[i]->index, DataStreams[^1]);

                        break;

                    default:
                        Log.Info($"#[Unknown #{i}] {fmtCtx->streams[i]->codecpar->codec_type}");
                        break;
                }
            }

            if (!audioHasEng)
                for (int i=0; i<AudioStreams.Count; i++)
                    if (AudioStreams[i].Language.Culture == null && AudioStreams[i].Language.OriginalInput == null)
                        AudioStreams[i].Language = Language.English;

            if (!subsHasEng && Type == MediaType.Video)
                for (int i=0; i<SubtitlesStreamsAll.Count; i++)
                    if (SubtitlesStreamsAll[i].Language.Culture == null && SubtitlesStreamsAll[i].Language.OriginalInput == null)
                        SubtitlesStreamsAll[i].Language = Language.English;

            if (fmtCtx->nb_programs > 0)
            {
                for (int i = 0; i < fmtCtx->nb_programs; i++)
                {
                    fmtCtx->programs[i]->discard = AVDiscard.All;
                    Program program = new(fmtCtx->programs[i], this);
                    Programs.Add(program);
                }
            }

            Extension = GetValidExtension();
        }

        PrintDump();
        return hasVideo;
    }

    public int SeekInQueue(long ticks, bool forward = false)
    {
        lock (lockActions)
        {
            if (Disposed) return -1;

            /* Seek within current bufffered queue
             *
             * 10 seconds because of video keyframe distances or 1 seconds for other (can be fixed also with CurTime+X seek instead of timestamps)
             * For subtitles it should keep (prev packet) the last presented as it can have a lot of distance with CurTime (cur packet)
             * It doesn't work for HLS live streams
             * It doesn't work for decoders buffered queue (which is small only subs might be an issue if we have large decoder queue)
             */

            long startTime = StartTime;

            if (hlsCtx != null)
            {
                ticks    += hlsStartTime - (hlsCtx->first_timestamp * 10);
                startTime = hlsStartTime;
            }

            if (ticks + (VideoStream != null && forward ? (10000 * 10000) : 1000 * 10000) > CurTime + startTime && ticks < CurTime + startTime + BufferedDuration)
            {
                bool found = false;
                while (VideoPackets.Count > 0)
                {
                    var packet = VideoPackets.Peek();
                    if (packet->pts != AV_NOPTS_VALUE && ticks < packet->pts * VideoStream.Timebase && (packet->flags & PktFlags.Key) != 0)
                    {
                        found = true;
                        ticks = (long) (packet->pts * VideoStream.Timebase);

                        break;
                    }

                    VideoPackets.Dequeue();
                    av_packet_free(&packet);
                }

                while (AudioPackets.Count > 0)
                {
                    var packet = AudioPackets.Peek();
                    if (packet->pts != AV_NOPTS_VALUE && (packet->pts + packet->duration) * AudioStream.Timebase >= ticks)
                    {
                        if (Type == MediaType.Audio || VideoStream == null)
                            found = true;

                        break;
                    }

                    AudioPackets.Dequeue();
                    av_packet_free(&packet);
                }

                for (int i = 0; i < subNum; i++)
                {
                    while (SubtitlesPackets[i].Count > 0)
                    {
                        var packet = SubtitlesPackets[i].Peek();
                        if (packet->pts != AV_NOPTS_VALUE && ticks < (packet->pts + packet->duration) * SubtitlesStreams[i].Timebase)
                        {
                            if (Type == MediaType.Subs)
                            {
                                found = true;
                            }

                            break;
                        }

                        SubtitlesPackets[i].Dequeue();
                        av_packet_free(&packet);
                    }
                }

                while (DataPackets.Count > 0)
                {
                    var packet = DataPackets.Peek();
                    if (packet->pts != AV_NOPTS_VALUE && ticks < (packet->pts + packet->duration) * DataStream.Timebase)
                    {
                        if (Type == MediaType.Data)
                            found = true;

                        break;
                    }

                    DataPackets.Dequeue();
                    av_packet_free(&packet);
                }

                if (found)
                {
                    Log.Debug("[Seek] Found in Queue");
                    return 0;
                }
            }

            return -1;
        }
    }
    public int Seek(long ticks, bool forward = false)
    {
        /* Current Issues
         *
         * HEVC/MPEG-TS: Fails to seek to keyframe https://blog.csdn.net/Annie_heyeqq/article/details/113649501 | https://trac.ffmpeg.org/ticket/9412
         * AVSEEK_FLAG_BACKWARD will not work on .dav even if it returns 0 (it will work after it fills the index table)
         * Strange delay (could be 200ms!) after seek on HEVC/yuv420p10le (10-bits) while trying to Present on swapchain (possible recreates texturearray?)
         * AVFMT_NOTIMESTAMPS unknown duration (can be calculated?) should perform byte seek instead (percentage based on total pb size)
         */

        lock (lockActions)
        {
            if (Disposed) return -1;

            int ret;
            long savedPbPos = 0;

            Interrupter.ForceInterrupt = 1;
            lock (lockFmtCtx)
            {
                Interrupter.ForceInterrupt = 0;

                // Flush required because of the interrupt
                if (fmtCtx->pb != null)
                {
                    savedPbPos = fmtCtx->pb->pos;
                    avio_flush(fmtCtx->pb);
                    fmtCtx->pb->error = 0; // AVERROR_EXIT will stay forever and will cause the demuxer to go in Status Stopped instead of Ended (after interrupted seeks)
                    fmtCtx->pb->eof_reached = 0;
                }
                avformat_flush(fmtCtx);

                // Forces seekable HLS
                if (hlsCtx != null)
                    fmtCtx->ctx_flags &= ~FmtCtxFlags.Unseekable;

                Interrupter.SeekRequest();
                if (VideoStream != null)
                {
                    if (CanDebug) Log.Debug($"[Seek({(forward ? "->" : "<-")})] Requested at {new TimeSpan(ticks)}");

                    // TODO: After proper calculation of Duration
                    //if (VideoStream.FixTimestamps && Duration > 0)
                        //ret = av_seek_frame(fmtCtx, -1, (long)((ticks/(double)Duration) * avio_size(fmtCtx->pb)), AVSEEK_FLAG_BYTE);
                    //else
                    ret = ticks == StartTime // we should also call this if we seek anywhere within the first Gop
                        ? avformat_seek_file(fmtCtx, -1, 0, 0, 0, 0)
                        : av_seek_frame(fmtCtx, -1, ticks / 10, forward ? SeekFlags.Frame : SeekFlags.Backward);

                    curReverseStopPts = AV_NOPTS_VALUE;
                    curReverseStartPts= AV_NOPTS_VALUE;
                }
                else
                {
                    if (CanDebug) Log.Debug($"[Seek({(forward ? "->" : "<-")})] Requested at {new TimeSpan(ticks)} | ANY");
                    ret = forward ?
                        avformat_seek_file(fmtCtx, -1, ticks / 10   , ticks / 10, long.MaxValue , SeekFlags.Any):
                        avformat_seek_file(fmtCtx, -1, long.MinValue, ticks / 10, ticks / 10    , SeekFlags.Any);
                }

                if (ret < 0)
                {
                    if (hlsCtx != null) fmtCtx->ctx_flags &= ~FmtCtxFlags.Unseekable;
                    Log.Info($"Seek failed 1/2 (retrying) {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    ret = VideoStream != null
                        ? av_seek_frame(fmtCtx, -1, ticks / 10, forward ? SeekFlags.Backward : SeekFlags.Frame)
                        : forward ?
                            avformat_seek_file(fmtCtx, -1, long.MinValue, ticks / 10, ticks / 10    , SeekFlags.Any):
                            avformat_seek_file(fmtCtx, -1, ticks / 10   , ticks / 10, long.MaxValue , SeekFlags.Any);

                    if (ret < 0)
                    {
                        Log.Warn($"Seek failed 2/2 {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                        // Flush required because of seek failure (reset pb to last pos otherwise will be eof) - Mainly for NoTimestamps (TODO: byte seek/calc dur/percentage)
                        if (fmtCtx->pb != null)
                        {
                            avio_flush(fmtCtx->pb);
                            fmtCtx->pb->error = 0;
                            fmtCtx->pb->eof_reached = 0;
                            avio_seek(fmtCtx->pb, savedPbPos, 0);
                        }
                        avformat_flush(fmtCtx);
                    }
                    else
                        lastSeekTime = ticks - StartTime - (hlsCtx != null ? hlsStartTime : 0);
                }
                else
                    lastSeekTime = ticks - StartTime - (hlsCtx != null ? hlsStartTime : 0);

                DisposePackets();
                lock (lockStatus) if (Status == Status.Ended) Status = Status.Stopped;
            }

            return ret; // >= 0 for success
        }
    }

    protected override void RunInternal()
    {
        if (IsReversePlayback)
        {
            RunInternalReverse();
            return;
        }

        int ret = 0;
        int allowedErrors = Config.MaxErrors;
        bool gotAVERROR_EXIT = false;
        audioBufferLimitFired = false;
        long lastVideoPacketPts = 0;

        do
        {
            // Wait until not QueueFull
            if (BufferedDuration > Config.BufferDuration || (Config.BufferPackets != 0 && CurPackets.Count > Config.BufferPackets))
            {
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueFull;

                while (!PauseOnQueueFull && (BufferedDuration > Config.BufferDuration || (Config.BufferPackets != 0 && CurPackets.Count > Config.BufferPackets)) && Status == Status.QueueFull)
                    Thread.Sleep(20);

                lock (lockStatus)
                {
                    if (PauseOnQueueFull) { PauseOnQueueFull = false; Status = Status.Pausing; }
                    if (Status != Status.QueueFull) break;
                    Status = Status.Running;
                }
            }

            // Wait possible someone asks for lockFmtCtx
            else if (gotAVERROR_EXIT)
            {
                gotAVERROR_EXIT = false;
                Thread.Sleep(5);
            }

            // Demux Packet
            lock (lockFmtCtx)
            {
                Interrupter.ReadRequest();
                ret = av_read_frame(fmtCtx, packet);
                if (Interrupter.ForceInterrupt != 0)
                {
                    av_packet_unref(packet);
                    gotAVERROR_EXIT = true;
                    continue;
                }

                // Possible check if interrupt/timeout and we dont seek to reset the backend pb->pos = 0?
                if (ret != 0)
                {
                    av_packet_unref(packet);

                    if (ret == AVERROR_EOF)
                    {
                        Status = Status.Ended;
                        break;
                    }

                    if (Interrupter.Timedout)
                    {
                        Status = Status.Stopping;
                        break;
                    }

                    allowedErrors--;
                    if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                    gotAVERROR_EXIT = true;
                    continue;
                }

                TotalBytes += packet->size;

                // Skip Disabled Streams | TODO: It's possible that the streams will changed (add/remove or even update/change of codecs)
                if (!EnabledStreams.Contains(packet->stream_index)) { av_packet_unref(packet); continue; }

                if (IsHLSLive)
                    UpdateHLSTime();

                if (CanTrace)
                {
                    var stream = AVStreamToStream[packet->stream_index];
                    long dts = packet->dts == AV_NOPTS_VALUE ? -1 : (long)(packet->dts * stream.Timebase);
                    long pts = packet->pts == AV_NOPTS_VALUE ? -1 : (long)(packet->pts * stream.Timebase);
                    Log.Trace($"[{stream.Type}] DTS: {(dts == -1 ? "-" : Utils.TicksToTime(dts))} PTS: {(pts == -1 ? "-" : Utils.TicksToTime(pts))} | FLPTS: {(pts == -1 ? "-" : Utils.TicksToTime(pts - StartTime))} | CurTime: {Utils.TicksToTime(CurTime)} | Buffered: {Utils.TicksToTime(BufferedDuration)}");
                }

                // Enqueue Packet (AVS Queue or Single Queue)
                if (UseAVSPackets)
                {
                    switch (fmtCtx->streams[packet->stream_index]->codecpar->codec_type)
                    {
                        case AVMediaType.Audio:
                            //Log($"Audio => {Utils.TicksToTime((long)(packet->pts * AudioStream.Timebase))} | {Utils.TicksToTime(CurTime)}");

                            // Handles A/V de-sync and ffmpeg bug with 2^33 timestamps wrapping
                            if (Config.MaxAudioPackets != 0 && AudioPackets.Count > Config.MaxAudioPackets)
                            {
                                av_packet_unref(packet);
                                packet = av_packet_alloc();

                                if (!audioBufferLimitFired)
                                {
                                    audioBufferLimitFired = true;
                                    OnAudioLimit();
                                }

                                break;
                            }

                            AudioPackets.Enqueue(packet);
                            packet = av_packet_alloc();

                            break;

                        case AVMediaType.Video:
                            //Log($"Video => {Utils.TicksToTime((long)(packet->pts * VideoStream.Timebase))} | {Utils.TicksToTime(CurTime)}");
                            lastVideoPacketPts = packet->pts;
                            VideoPackets.Enqueue(packet);
                            packet = av_packet_alloc();

                            break;

                        case AVMediaType.Subtitle:
                            for (int i = 0; i < subNum; i++)
                            {
                                // Clone packets to support simultaneous display of the same subtitle
                                if (packet->stream_index == SubtitlesStreams[i]?.StreamIndex)
                                {
                                    SubtitlesPackets[i].Enqueue(av_packet_clone(packet));
                                }
                            }

                            // cloned, so free packet
                            av_packet_unref(packet);

                            break;

                        case AVMediaType.Data:
                            // Some data streams only have nopts, set pts to last video packet pts
                            if (packet->pts == AV_NOPTS_VALUE)
                                packet->pts = lastVideoPacketPts;
                            DataPackets.Enqueue(packet);
                            packet = av_packet_alloc();

                            break;

                        default:
                            av_packet_unref(packet);
                            break;
                    }
                }
                else
                {
                    Packets.Enqueue(packet);
                    packet = av_packet_alloc();
                }
            }
        } while (Status == Status.Running);
    }
    private void RunInternalReverse()
    {
        int ret = 0;
        int allowedErrors = Config.MaxErrors;
        bool gotAVERROR_EXIT = false;

        // To demux further for buffering (related to BufferDuration)
        int maxQueueSize = 2;
        curReverseSeekOffset = av_rescale_q(3 * 1000 * 10000 / 10, Engine.FFmpeg.AV_TIMEBASE_Q, VideoStream.AVStream->time_base);

        do
        {
            // Wait until not QueueFull
            if (VideoPacketsReverse.Count > maxQueueSize)
            {
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueFull;

                while (!PauseOnQueueFull && VideoPacketsReverse.Count > maxQueueSize && Status == Status.QueueFull) { Thread.Sleep(20); }

                lock (lockStatus)
                {
                    if (PauseOnQueueFull) { PauseOnQueueFull = false; Status = Status.Pausing; }
                    if (Status != Status.QueueFull) break;
                    Status = Status.Running;
                }
            }

            // Wait possible someone asks for lockFmtCtx
            else if (gotAVERROR_EXIT)
            {
                gotAVERROR_EXIT = false;
                Thread.Sleep(5);
            }

            // Demux Packet
            lock (lockFmtCtx)
            {
                Interrupter.ReadRequest();
                ret = av_read_frame(fmtCtx, packet);
                if (Interrupter.ForceInterrupt != 0)
                {
                    av_packet_unref(packet); gotAVERROR_EXIT = true;
                    continue;
                }

                // Possible check if interrupt/timeout and we dont seek to reset the backend pb->pos = 0?
                if (ret != 0)
                {
                    av_packet_unref(packet);

                    if (ret == AVERROR_EOF)
                    {
                        if (curReverseVideoPackets.Count > 0)
                        {
                            var drainPacket = av_packet_alloc();
                            drainPacket->data = null;
                            drainPacket->size = 0;
                            curReverseVideoPackets.Add((IntPtr)drainPacket);
                            curReverseVideoStack.Push(curReverseVideoPackets);
                            curReverseVideoPackets = new List<IntPtr>();
                        }

                        if (!curReverseVideoStack.IsEmpty)
                        {
                            VideoPacketsReverse.Enqueue(curReverseVideoStack);
                            curReverseVideoStack = new ConcurrentStack<List<IntPtr>>();
                        }

                        if (curReverseStartPts != AV_NOPTS_VALUE && curReverseStartPts <= VideoStream.StartTimePts)
                        {
                            Status = Status.Ended;
                            break;
                        }

                        //Log($"[][][SEEK END] {curReverseStartPts} | {Utils.TicksToTime((long) (curReverseStartPts * VideoStream.Timebase))}");
                        Interrupter.SeekRequest();
                        ret = av_seek_frame(fmtCtx, VideoStream.StreamIndex, Math.Max(curReverseStartPts - curReverseSeekOffset, VideoStream.StartTimePts), SeekFlags.Backward);

                        if (ret != 0)
                        {
                            Status = Status.Stopping;
                            break;
                        }

                        curReverseStopPts = curReverseStartPts;
                        curReverseStartPts = AV_NOPTS_VALUE;
                        continue;
                    }

                    allowedErrors--;
                    if (CanWarn) Log.Warn($"{ FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                    gotAVERROR_EXIT = true;
                    continue;
                }

                if (VideoStream.StreamIndex != packet->stream_index) { av_packet_unref(packet); continue; }

                if ((packet->flags & PktFlags.Key) != 0)
                {
                    if (curReverseStartPts == AV_NOPTS_VALUE)
                        curReverseStartPts = packet->pts;

                    if (curReverseVideoPackets.Count > 0)
                    {
                        var drainPacket = av_packet_alloc();
                        drainPacket->data = null;
                        drainPacket->size = 0;
                        curReverseVideoPackets.Add((IntPtr)drainPacket);
                        curReverseVideoStack.Push(curReverseVideoPackets);
                        curReverseVideoPackets = new List<IntPtr>();
                    }
                }

                if (packet->pts != AV_NOPTS_VALUE && (
                    (curReverseStopRequestedPts != AV_NOPTS_VALUE && curReverseStopRequestedPts <= packet->pts)  ||
                    (curReverseStopPts == AV_NOPTS_VALUE && (packet->flags & PktFlags.Key) != 0 && packet->pts != curReverseStartPts)     ||
                    (packet->pts == curReverseStopPts)
                    ))
                {
                    if (curReverseStartPts == AV_NOPTS_VALUE || curReverseStopPts == curReverseStartPts)
                    {
                        curReverseSeekOffset *= 2;
                        if (curReverseStartPts == AV_NOPTS_VALUE) curReverseStartPts = curReverseStopPts;
                        if (curReverseStartPts == AV_NOPTS_VALUE) curReverseStartPts = curReverseStopRequestedPts;
                    }

                    curReverseStopRequestedPts = AV_NOPTS_VALUE;

                    if ((packet->flags & PktFlags.Key) == 0 && curReverseVideoPackets.Count > 0)
                    {
                        var drainPacket = av_packet_alloc();
                        drainPacket->data = null;
                        drainPacket->size = 0;
                        curReverseVideoPackets.Add((IntPtr)drainPacket);
                        curReverseVideoStack.Push(curReverseVideoPackets);
                        curReverseVideoPackets = new List<IntPtr>();
                    }

                    if (!curReverseVideoStack.IsEmpty)
                    {
                        VideoPacketsReverse.Enqueue(curReverseVideoStack);
                        curReverseVideoStack = new ConcurrentStack<List<IntPtr>>();
                    }

                    av_packet_unref(packet);

                    if (curReverseStartPts != AV_NOPTS_VALUE && curReverseStartPts <= VideoStream.StartTimePts)
                    {
                        Status = Status.Ended;
                        break;
                    }

                    //Log($"[][][SEEK] {curReverseStartPts} | {Utils.TicksToTime((long) (curReverseStartPts * VideoStream.Timebase))}");
                    Interrupter.SeekRequest();
                    ret = av_seek_frame(fmtCtx, VideoStream.StreamIndex, Math.Max(curReverseStartPts - curReverseSeekOffset, 0), SeekFlags.Backward);

                    if (ret != 0)
                    {
                        Status = Status.Stopping;
                        break;
                    }

                    curReverseStopPts = curReverseStartPts;
                    curReverseStartPts = AV_NOPTS_VALUE;
                }
                else
                {
                    if (curReverseStartPts != AV_NOPTS_VALUE)
                    {
                        curReverseVideoPackets.Add((IntPtr)packet);
                        packet = av_packet_alloc();
                    }
                    else
                        av_packet_unref(packet);
                }
            }

        } while (Status == Status.Running);
    }

    public void EnableReversePlayback(long timestamp)
    {
        IsReversePlayback = true;
        Seek(StartTime + timestamp);
        curReverseStopRequestedPts = av_rescale_q((StartTime + timestamp) / 10, Engine.FFmpeg.AV_TIMEBASE_Q, VideoStream.AVStream->time_base);
    }
    public void DisableReversePlayback() => IsReversePlayback = false;
    #endregion

    #region Switch Programs / Streams
    public bool IsProgramEnabled(StreamBase stream)
    {
        for (int i=0; i<Programs.Count; i++)
            for (int l=0; l<Programs[i].Streams.Count; l++)
                if (Programs[i].Streams[l].StreamIndex == stream.StreamIndex && fmtCtx->programs[i]->discard != AVDiscard.All)
                    return true;

        return false;
    }
    public void EnableProgram(StreamBase stream)
    {
        if (IsProgramEnabled(stream))
        {
            if (CanDebug) Log.Debug($"[Stream #{stream.StreamIndex}] Program already enabled");
            return;
        }

        for (int i=0; i<Programs.Count; i++)
            for (int l=0; l<Programs[i].Streams.Count; l++)
                if (Programs[i].Streams[l].StreamIndex == stream.StreamIndex)
                {
                    if (CanDebug) Log.Debug($"[Stream #{stream.StreamIndex}] Enables program #{i}");
                    fmtCtx->programs[i]->discard = AVDiscard.Default;
                    return;
                }
    }
    public void DisableProgram(StreamBase stream)
    {
        for (int i=0; i<Programs.Count; i++)
            for (int l=0; l<Programs[i].Streams.Count; l++)
                if (Programs[i].Streams[l].StreamIndex == stream.StreamIndex && fmtCtx->programs[i]->discard != AVDiscard.All)
                {
                    bool isNeeded = false;
                    for (int l2=0; l2<Programs[i].Streams.Count; l2++)
                    {
                        if (Programs[i].Streams[l2].StreamIndex != stream.StreamIndex && EnabledStreams.Contains(Programs[i].Streams[l2].StreamIndex))
                            {isNeeded = true; break; }
                    }

                    if (!isNeeded)
                    {
                        if (CanDebug) Log.Debug($"[Stream #{stream.StreamIndex}] Disables program #{i}");
                        fmtCtx->programs[i]->discard = AVDiscard.All;
                    }
                    else if (CanDebug)
                        Log.Debug($"[Stream #{stream.StreamIndex}] Program #{i} is needed");
                }
    }

    public void EnableStream(StreamBase stream)
    {
        lock (lockFmtCtx)
        {
            if (Disposed || stream == null || EnabledStreams.Contains(stream.StreamIndex))
            {
                // If it detects that the primary and secondary are trying to select the same subtitle
                // Put the same instance in SubtitlesStreams and return.
                if (stream != null && stream.Type == MediaType.Subs && EnabledStreams.Contains(stream.StreamIndex))
                {
                    SubtitlesStreams[SubtitlesSelectedHelper.CurIndex] = (SubtitlesStream)stream;
                    // Necessary to update UI immediately
                    stream.Enabled = stream.Enabled;
                }
                return;
            };

            EnabledStreams.Add(stream.StreamIndex);
            fmtCtx->streams[stream.StreamIndex]->discard = AVDiscard.Default;
            stream.Enabled = true;
            EnableProgram(stream);

            switch (stream.Type)
            {
                case MediaType.Audio:
                    AudioStream = (AudioStream) stream;
                    if (VideoStream == null)
                    {
                        if (AudioStream.HLSPlaylist != null)
                        {
                            IsHLSLive = true;
                            HLSPlaylist = AudioStream.HLSPlaylist;
                            UpdateHLSTime();
                        }
                        else
                        {
                            HLSPlaylist = null;
                            IsHLSLive = false;
                        }
                    }

                    break;

                case MediaType.Video:
                    VideoStream = (VideoStream) stream;
                    VideoPackets.frameDuration = VideoStream.FrameDuration > 0 ? VideoStream.FrameDuration : 30 * 1000 * 10000;
                    if (VideoStream.HLSPlaylist != null)
                    {
                        IsHLSLive = true;
                        HLSPlaylist = VideoStream.HLSPlaylist;
                        UpdateHLSTime();
                    }
                    else
                    {
                        HLSPlaylist = null;
                        IsHLSLive = false;
                    }

                    break;

                case MediaType.Subs:
                    SubtitlesStreams[SubtitlesSelectedHelper.CurIndex] = (SubtitlesStream)stream;

                    break;

                case MediaType.Data:
                    DataStream = (DataStream) stream;

                    break;
            }

            if (UseAVSPackets)
                CurPackets = VideoStream != null ? VideoPackets : (AudioStream != null ? AudioPackets : (SubtitlesStreams[0] != null ? SubtitlesPackets[0] : DataPackets));

            if (CanInfo) Log.Info($"[{stream.Type} #{stream.StreamIndex}] Enabled");
        }
    }
    public void DisableStream(StreamBase stream)
    {
        lock (lockFmtCtx)
        {
            if (Disposed || stream == null || !EnabledStreams.Contains(stream.StreamIndex)) return;

            // If it is the same subtitle, do not disable it.
            if (stream.Type == MediaType.Subs && SubtitlesStreams[0] != null && SubtitlesStreams[0] == SubtitlesStreams[1])
            {
                // clear only what is needed
                SubtitlesStreams[SubtitlesSelectedHelper.CurIndex] = null;
                SubtitlesPackets[SubtitlesSelectedHelper.CurIndex].Clear();
                // Necessary to update UI immediately
                stream.Enabled = stream.Enabled;
                return;
            }

            /* AVDISCARD_ALL causes syncing issues between streams (TBR: bandwidth?)
             * 1) While switching video streams will not switch at the same timestamp
             * 2) By disabling video stream after a seek, audio will not seek properly
             * 3) HLS needs to update hlsCtx->first_time and read at least on package before seek to be accurate
             */

            fmtCtx->streams[stream.StreamIndex]->discard = AVDiscard.All;
            EnabledStreams.Remove(stream.StreamIndex);
            stream.Enabled = false;
            DisableProgram(stream);

            switch (stream.Type)
            {
                case MediaType.Audio:
                    AudioStream = null;
                    if (VideoStream != null)
                    {
                        if (VideoStream.HLSPlaylist != null)
                        {
                            IsHLSLive = true;
                            HLSPlaylist = VideoStream.HLSPlaylist;
                            UpdateHLSTime();
                        }
                        else
                        {
                            HLSPlaylist = null;
                            IsHLSLive = false;
                        }
                    }

                    AudioPackets.Clear();

                    break;

                case MediaType.Video:
                    VideoStream = null;
                    if (AudioStream != null)
                    {
                        if (AudioStream.HLSPlaylist != null)
                        {
                            IsHLSLive = true;
                            HLSPlaylist = AudioStream.HLSPlaylist;
                            UpdateHLSTime();
                        }
                        else
                        {
                            HLSPlaylist = null;
                            IsHLSLive = false;
                        }
                    }

                    VideoPackets.Clear();
                    break;

                case MediaType.Subs:
                    SubtitlesStreams[SubtitlesSelectedHelper.CurIndex] = null;
                    SubtitlesPackets[SubtitlesSelectedHelper.CurIndex].Clear();

                    break;

                case MediaType.Data:
                    DataStream = null;
                    DataPackets.Clear();

                    break;
            }

            if (UseAVSPackets)
                CurPackets = VideoStream != null ? VideoPackets : (AudioStream != null ? AudioPackets : SubtitlesPackets[0]);

            if (CanInfo) Log.Info($"[{stream.Type} #{stream.StreamIndex}] Disabled");
        }
    }
    public void SwitchStream(StreamBase stream)
    {
        lock (lockFmtCtx)
        {
            if (stream.Type == MediaType.Audio)
                DisableStream(AudioStream);
            else if (stream.Type == MediaType.Video)
                DisableStream(VideoStream);
            else if (stream.Type == MediaType.Subs)
                DisableStream(SubtitlesStreams[SubtitlesSelectedHelper.CurIndex]);
            else
                DisableStream(DataStream);

            EnableStream(stream);
        }
    }
    #endregion

    #region Misc
    internal void UpdateHLSTime()
    {
        // TBR: Access Violation
        // [hls @ 00000269f9cdb400] Media sequence changed unexpectedly: 150070 -> 0

        if (hlsPrevSeqNo != HLSPlaylist->cur_seq_no)
        {
            hlsPrevSeqNo = HLSPlaylist->cur_seq_no;
            hlsStartTime = AV_NOPTS_VALUE;

            hlsCurDuration = 0;
            long duration = 0;

            for (long i=0; i<HLSPlaylist->cur_seq_no - HLSPlaylist->start_seq_no; i++)
            {
                hlsCurDuration += HLSPlaylist->segments[i]->duration;
                duration += HLSPlaylist->segments[i]->duration;
            }

            for (long i=HLSPlaylist->cur_seq_no - HLSPlaylist->start_seq_no; i<HLSPlaylist->n_segments; i++)
                duration += HLSPlaylist->segments[i]->duration;

            hlsCurDuration *= 10;
            duration *= 10;
            Duration = duration;
        }

        if (hlsStartTime == AV_NOPTS_VALUE && CurPackets.LastTimestamp != AV_NOPTS_VALUE)
        {
            hlsStartTime = CurPackets.LastTimestamp - hlsCurDuration;
            CurPackets.UpdateCurTime();
        }
    }

    private string GetValidExtension()
    {
        // TODO
        // Should check for all supported output formats (there is no list in ffmpeg.autogen ?)
        // Should check for inner input format (not outer protocol eg. hls/rtsp)
        // Should check for raw codecs it can be mp4/mov but it will not work in mp4 only in mov (or avi for raw)

        if (Name == "mpegts")
            return "ts";
        else if (Name == "mpeg")
            return "mpeg";

        List<string> supportedOutput = new() { "mp4", "avi", "flv", "flac", "mpeg", "mpegts", "mkv", "ogg", "ts"};
        string defaultExtenstion = "mp4";
        bool hasPcm = false;
        bool isRaw = false;

        foreach (var stream in AudioStreams)
            if (stream.Codec.Contains("pcm")) hasPcm = true;

        foreach (var stream in VideoStreams)
            if (stream.Codec.Contains("raw")) isRaw = true;

        if (isRaw) defaultExtenstion = "avi";

        // MP4 container doesn't support PCM
        if (hasPcm) defaultExtenstion = "mkv";

        // TODO
        // Check also shortnames
        //if (Name == "mpegts") return "ts";
        //if ((fmtCtx->iformat->flags & AVFMT_TS_DISCONT) != 0) should be mp4 or container that supports segments

        if (string.IsNullOrEmpty(Extensions)) return defaultExtenstion;
        string[] extensions = Extensions.Split(',');
        if (extensions == null || extensions.Length < 1) return defaultExtenstion;

        // Try to set the output container same as input
        for (int i=0; i<extensions.Length; i++)
            if (supportedOutput.Contains(extensions[i]))
                return extensions[i] == "mp4" && isRaw ? "mov" : extensions[i];

        return defaultExtenstion;
    }
    private void PrintDump()
    {
        string dump = $"\r\n[Format  ] {LongName}/{Name} | {Extensions} | {Utils.TicksToTime(fmtCtx->start_time * 10)}/{Utils.TicksToTime(fmtCtx->duration * 10)} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}";

        foreach(var stream in VideoStreams)     dump += "\r\n" + stream.GetDump();
        foreach(var stream in AudioStreams)     dump += "\r\n" + stream.GetDump();
        foreach(var stream in SubtitlesStreamsAll) dump += "\r\n" + stream.GetDump();

        if (fmtCtx->nb_programs > 0)
            dump += $"\r\n[Programs] {fmtCtx->nb_programs}";

        for (int i=0; i<fmtCtx->nb_programs; i++)
        {
            dump += $"\r\n\tProgram #{i}";

            if (fmtCtx->programs[i]->nb_stream_indexes > 0)
                dump += $"\r\n\t\tStreams [{fmtCtx->programs[i]->nb_stream_indexes}]: ";

            for (int l=0; l<fmtCtx->programs[i]->nb_stream_indexes; l++)
                dump += $"{fmtCtx->programs[i]->stream_index[l]},";

            if (fmtCtx->programs[i]->nb_stream_indexes > 0)
                dump = dump[..^1];
        }

        if (CanInfo) Log.Info($"Format Context Info {dump}\r\n");
    }

    /// <summary>
    /// Gets next VideoPacket from the existing queue or demuxes it if required (Demuxer must not be running)
    /// </summary>
    /// <returns>0 on success</returns>
    public int GetNextVideoPacket()
    {
        if (VideoPackets.Count > 0)
        {
            packet = VideoPackets.Dequeue();
            return 0;
        }
        else
            return GetNextPacket(VideoStream.StreamIndex);
    }

    /// <summary>
    /// Pushes the demuxer to the next available packet (Demuxer must not be running)
    /// </summary>
    /// <param name="streamIndex">Packet's stream index</param>
    /// <returns>0 on success</returns>
    public int GetNextPacket(int streamIndex = -1)
    {
        int ret;

        while (true)
        {
            Interrupter.ReadRequest();
            ret = av_read_frame(fmtCtx, packet);

            if (ret != 0)
            {
                av_packet_unref(packet);

                if ((ret == AVERROR_EXIT && fmtCtx->pb != null && fmtCtx->pb->eof_reached != 0) || ret == AVERROR_EOF)
                {
                    packet = av_packet_alloc();
                    packet->data = null;
                    packet->size = 0;

                    Stop();
                    Status = Status.Ended;
                }

                return ret;
            }

            if (streamIndex != -1)
            {
                if (packet->stream_index == streamIndex)
                    return 0;
            }
            else if (EnabledStreams.Contains(packet->stream_index))
                return 0;

            av_packet_unref(packet);
        }
    }
    #endregion
}

public unsafe class PacketQueue
{
    // TODO: DTS might not be available without avformat_find_stream_info (should changed based on packet->duration and fallback should be removed)
    readonly Demuxer demuxer;
    readonly ConcurrentQueue<IntPtr> packets = new();
    public long frameDuration = 30 * 1000 * 10000; // in case of negative buffer duration calculate it based on packets count / FPS

    public long Bytes               { get; private set; }
    public long BufferedDuration    { get; private set; }
    public long CurTime             { get; private set; }
    public int  Count               => packets.Count;
    public bool IsEmpty             => packets.IsEmpty;

    public long FirstTimestamp      { get; private set; } = AV_NOPTS_VALUE;
    public long LastTimestamp       { get; private set; } = AV_NOPTS_VALUE;

    public PacketQueue(Demuxer demuxer)
        => this.demuxer = demuxer;

    public void Clear()
    {
        lock(packets)
        {
            while (!packets.IsEmpty)
            {
                packets.TryDequeue(out IntPtr packetPtr);
                if (packetPtr == IntPtr.Zero) continue;
                AVPacket* packet = (AVPacket*)packetPtr;
                av_packet_free(&packet);
            }

            FirstTimestamp = AV_NOPTS_VALUE;
            LastTimestamp = AV_NOPTS_VALUE;
            Bytes = 0;
            BufferedDuration = 0;
            CurTime = 0;
        }
    }

    public void Enqueue(AVPacket* packet)
    {
        lock (packets)
        {
            packets.Enqueue((IntPtr)packet);

            if (packet->dts != AV_NOPTS_VALUE || packet->pts != AV_NOPTS_VALUE)
            {
                LastTimestamp = packet->dts != AV_NOPTS_VALUE ?
                    (long)(packet->dts * demuxer.AVStreamToStream[packet->stream_index].Timebase):
                    (long)(packet->pts * demuxer.AVStreamToStream[packet->stream_index].Timebase);

                if (FirstTimestamp == AV_NOPTS_VALUE)
                {
                    FirstTimestamp = LastTimestamp;
                    UpdateCurTime();
                }
                else
                {
                    BufferedDuration = LastTimestamp - FirstTimestamp;
                    if (BufferedDuration < 0)
                        BufferedDuration = packets.Count * frameDuration;
                }
            }
            else
                BufferedDuration = packets.Count * frameDuration;

            Bytes += packet->size;
        }
    }

    public AVPacket* Dequeue()
    {
        lock(packets)
            if (packets.TryDequeue(out IntPtr packetPtr))
            {
                AVPacket* packet = (AVPacket*)packetPtr;

                if (packet->dts != AV_NOPTS_VALUE || packet->pts != AV_NOPTS_VALUE)
                {
                    FirstTimestamp = packet->dts != AV_NOPTS_VALUE ?
                        (long)(packet->dts * demuxer.AVStreamToStream[packet->stream_index].Timebase):
                        (long)(packet->pts * demuxer.AVStreamToStream[packet->stream_index].Timebase);
                    UpdateCurTime();
                }
                else
                    BufferedDuration = packets.Count * frameDuration;

                return (AVPacket*)packetPtr;
            }

        return null;
    }

    public AVPacket* Peek()
        => packets.TryPeek(out IntPtr packetPtr)
        ? (AVPacket*)packetPtr
        : (AVPacket*)null;

    internal void UpdateCurTime()
    {
        if (demuxer.hlsCtx != null)
        {
            if (demuxer.hlsStartTime != AV_NOPTS_VALUE)
            {
                if (FirstTimestamp < demuxer.hlsStartTime)
                {
                    demuxer.Duration += demuxer.hlsStartTime - FirstTimestamp;
                    demuxer.hlsStartTime = FirstTimestamp;
                    CurTime = 0;
                }
                else
                    CurTime = FirstTimestamp - demuxer.hlsStartTime;
            }
        }
        else
            CurTime = FirstTimestamp - demuxer.StartTime;

        if (CurTime < 0)
            CurTime = 0;

        BufferedDuration = LastTimestamp - FirstTimestamp;

        if (BufferedDuration < 0)
            BufferedDuration = packets.Count * frameDuration;
    }
}

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;
using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaPlayer.Translation;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

#nullable enable

public class SubtitlesManager
{
    private readonly SubManager[] _subManagers;
    public SubManager this[int subIndex] => _subManagers[subIndex];
    private readonly int _subNum;

    public SubtitlesManager(Config config, int subNum)
    {
        _subNum = subNum;
        _subManagers = new SubManager[subNum];
        for (int i = 0; i < subNum; i++)
        {
            _subManagers[i] = new SubManager(config, i);
        }
    }

    /// <summary>
    /// Open a file and read all subtitle data by streaming
    /// </summary>
    /// <param name="subIndex">0: Primary, 1: Secondary</param>
    /// <param name="url">subtitle file path or video file path</param>
    /// <param name="streamIndex">streamIndex of subtitle</param>
    /// <param name="type">demuxer media type</param>
    /// <param name="useBitmap">Use bitmap subtitles or immediately release bitmap if not used</param>
    /// <param name="lang">subtitle language</param>
    public void Open(int subIndex, string url, int streamIndex, MediaType type, bool useBitmap, Language lang)
    {
        // TODO: L: Add caching subtitle data for the same stream and URL?
        this[subIndex].Open(url, streamIndex, type, useBitmap, lang);
    }

    public void SetCurrentTime(TimeSpan currentTime)
    {
        for (int i = 0; i < _subNum; i++)
        {
            this[i].SetCurrentTime(currentTime);
        }
    }
}

public class SubManager : INotifyPropertyChanged
{
    private readonly Lock _locker = new();
    private CancellationTokenSource? _cts;
    public SubtitleData? SelectedSub { get; set => Set(ref field, value); }
    public int CurrentIndex { get; private set => Set(ref field, value); } = -1;

    public PositionState State
    {
        get;
        private set
        {
            bool prevIsDisplaying = IsDisplaying;
            if (Set(ref field, value) && prevIsDisplaying != IsDisplaying)
            {
                OnPropertyChanged(nameof(IsDisplaying));
            }
        }
    } = PositionState.First;

    public bool IsDisplaying => State == PositionState.Showing;

    /// <summary>
    /// List of subtitles that can be bound to ItemsControl
    /// Must be sorted with timestamp to perform binary search.
    /// </summary>
    public BulkObservableCollection<SubtitleData> Subs { get; } = new();

    /// <summary>
    /// True when addition to Subs is running... (Reading all subtitles, OCR, ASR)
    /// </summary>
    public bool IsLoading { get; private set => Set(ref field, value); }

    // LanguageSource with fallback
    public Language? Language
    {
        get
        {
            if (LanguageSource == Language.Unknown)
            {
                // fallback to user set language
                return _subIndex == 0 ? _config.Subtitles.LanguageFallbackPrimary : _config.Subtitles.LanguageFallbackSecondary;
            }

            return LanguageSource;
        }
    }

    public Language? LanguageSource
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(Language));
            }
        }
    }

    // For displaying bitmap subtitles, manage video width and height
    public int Width { get; internal set; }
    public int Height { get; internal set; }

    private readonly object _subsLocker = new();
    private readonly Config _config;
    private readonly int _subIndex;
    private readonly SubTranslator _subTranslator;
    private readonly LogHandler Log;

    public SubManager(Config config, int subIndex, bool enableSync = true)
    {
        _config = config;
        _subIndex = subIndex;
        // TODO: L: Review whether to initialize it here.
        _subTranslator = new SubTranslator(this, config.Subtitles, subIndex);
        Log = new LogHandler(("[#1]").PadRight(8, ' ') + $" [SubManager{subIndex + 1}   ] ");

        if (enableSync)
        {
            // Enable binding to ItemsControl
            Utils.UIInvokeIfRequired(() =>
            {
                BindingOperations.EnableCollectionSynchronization(Subs, _subsLocker);
            });
        }
    }

    public enum PositionState
    {
        First,   // still haven't reached the first subtitle
        Showing, // currently displaying
        Around,  // not displayed and can seek before and after
        Last     // After the last subtitle
    }

    /// <summary>
    /// Force UI refresh
    /// </summary>
    internal void Refresh()
    {
        // NOTE: If it is not executed in the main thread, the following error occurs.
        // System.NotSupportedException: 'This type of CollectionView does not support'
        Utils.UI(() =>
        {
            CollectionViewSource.GetDefaultView(Subs).Refresh();
            OnPropertyChanged(nameof(CurrentIndex)); // required for translating current sub
        });
    }

    /// <summary>
    /// This must be called when doing heavy operation
    /// </summary>
    /// <returns></returns>
    internal IDisposable StartLoading()
    {
        IsLoading = true;

        return Disposable.Create(() =>
        {
            IsLoading = false;
        });
    }

    public void Load(IEnumerable<SubtitleData> items)
    {
        lock (_subsLocker)
        {
            CurrentIndex = -1;
            SelectedSub = null;
            Subs.Clear();
            Subs.AddRange(items);
        }
    }

    public void Add(SubtitleData sub)
    {
        lock (_subsLocker)
        {
            sub.Index = Subs.Count;
            Subs.Add(sub);
        }
    }

    public void AddRange(IEnumerable<SubtitleData> items)
    {
        lock (_subsLocker)
        {
            Subs.AddRange(items);
        }
    }

    public SubtitleData? GetCurrent()
    {
        lock (_subsLocker)
        {
            if (Subs.Count == 0 || CurrentIndex == -1)
            {
                return null;
            }

            Debug.Assert(CurrentIndex < Subs.Count);

            if (State == PositionState.Showing)
            {
                return Subs[CurrentIndex];
            }

            return null;
        }
    }

    public SubtitleData? GetNext()
    {
        lock (_subsLocker)
        {
            if (Subs.Count == 0)
            {
                return null;
            }

            switch (State)
            {
                case PositionState.First:
                    return Subs[0];

                case PositionState.Showing:
                    if (CurrentIndex < Subs.Count - 1)
                        return Subs[CurrentIndex + 1];
                    break;

                case PositionState.Around:
                    if (CurrentIndex < Subs.Count - 1)
                        return Subs[CurrentIndex + 1];
                    break;
            }

            return null;
        }
    }

    public SubtitleData? GetPrev()
    {
        lock (_subsLocker)
        {
            if (Subs.Count == 0 || CurrentIndex == -1)
                return null;

            switch (State)
            {
                case PositionState.Showing:
                    if (CurrentIndex > 0)
                        return Subs[CurrentIndex - 1];
                    break;

                case PositionState.Around:
                    if (CurrentIndex >= 0)
                        return Subs[CurrentIndex];
                    break;

                case PositionState.Last:
                    return Subs[^1];
            }
        }

        return null;
    }

    private readonly SubtitleData _searchSub = new();

    public SubManager SetCurrentTime(TimeSpan currentTime)
    {
        // Adjust the display timing of subtitles by adjusting the timestamp of the video
        currentTime = currentTime.Subtract(new TimeSpan(_config.Subtitles[_subIndex].Delay));

        lock (_subsLocker)
        {
            // If no subtitle data is loaded, nothing is done.
            if (Subs.Count == 0)
                return this;

            // If it is a subtitle that is displaying, it does nothing.
            var curSub = GetCurrent();
            if (curSub != null && curSub.StartTime < currentTime && curSub.EndTime > currentTime)
            {
                return this;
            }

            _searchSub.StartTime = currentTime;

            int ret = Subs.BinarySearch(_searchSub, SubtitleTimeStartComparer.Instance);
            int cur = -1;

            if (~ret == 0)
            {
                CurrentIndex = -1;
                SelectedSub = null;
                State = PositionState.First;
                return this;
            }

            if (ret < 0)
            {
                // The reason subtracting 1 is that the result of the binary search is the next big index.
                cur = (~ret) - 1;
            }
            else
            {
                // If the starting position is matched, it is unlikely
                cur = ret;
            }

            Debug.Assert(cur >= 0, "negative index detected");
            Debug.Assert(cur < Subs.Count, "out of bounds detected");

            if (cur == Subs.Count - 1)
            {
                if (Subs[cur].EndTime < currentTime)
                {
                    CurrentIndex = cur;
                    SelectedSub = Subs[cur];
                    State = PositionState.Last;
                }
                else
                {
                    CurrentIndex = cur;
                    SelectedSub = Subs[cur];
                    State = PositionState.Showing;
                }
            }
            else
            {
                if (Subs[cur].StartTime <= currentTime && Subs[cur].EndTime >= currentTime)
                {
                    // Show subtitles
                    CurrentIndex = cur;
                    SelectedSub = Subs[cur];
                    State = PositionState.Showing;
                }
                else if (Subs[cur].StartTime <= currentTime)
                {
                    // Almost there to display in currentIndex.
                    CurrentIndex = cur;
                    SelectedSub = Subs[cur];
                    State = PositionState.Around;
                }
            }
        }

        return this;
    }

    public void Sort()
    {
        lock (_subsLocker)
        {
            if (Subs.Count == 0)
                return;

            Subs.Sort(SubtitleTimeStartComparer.Instance);
        }
    }

    public void DeleteAfter(TimeSpan time)
    {
        lock (_subsLocker)
        {
            if (Subs.Count == 0)
                return;

            int index = Subs.BinarySearch(new SubtitleData { EndTime = time }, new SubtitleTimeEndComparer());

            if (index < 0)
            {
                index = ~index;
            }

            if (index < Subs.Count)
            {
                var newSubs = Subs.GetRange(0, index).ToList();
                var deleteSubs = Subs.GetRange(index, Subs.Count - index).ToList();
                Load(newSubs);

                foreach (var sub in deleteSubs)
                {
                    sub.Dispose();
                }
            }
        }
    }

    public void Open(string url, int streamIndex, MediaType type, bool useBitmap, Language lang)
    {
        // Asynchronously read subtitle timestamps and text

        // Cancel if already executed
        TryCancelWait();

        lock (_locker)
        {
            using var loading = StartLoading();

            List<SubtitleData> subChunk = new();

            try
            {
                _cts = new CancellationTokenSource();
                using SubtitleReader reader = new(this, _config, _subIndex);
                reader.Open(url, streamIndex, type, _cts.Token);

                _cts.Token.ThrowIfCancellationRequested();

                bool isFirst = true;
                int subCnt = 0;

                Stopwatch refreshSw = new();
                refreshSw.Start();

                reader.ReadAll(useBitmap, data =>
                {
                    if (isFirst)
                    {
                        isFirst = false;
                        // Set the language at the timing of the first subtitle data set.
                        LanguageSource = lang;

                        Log.Info($"Start loading subs... (lang:{lang.TopEnglishName})");
                    }

                    data.Index = subCnt++;
                    subChunk.Add(data);

                    // Large files and network files take time to load to the end.
                    // To prevent frequent UI updates, use AddRange to group files to some extent before adding them.
                    if (subChunk.Count >= 2 && refreshSw.Elapsed > TimeSpan.FromMilliseconds(500))
                    {
                        AddRange(subChunk);
                        subChunk.Clear();
                        refreshSw.Restart();
                    }
                }, _cts.Token);

                // Process remaining
                if (subChunk.Count > 0)
                {
                    AddRange(subChunk);
                }
                refreshSw.Stop();
                Log.Info("End loading subs");
            }
            catch (OperationCanceledException)
            {
                foreach (var sub in subChunk)
                {
                    sub.Dispose();
                }

                Clear();
            }
        }
    }

    public void TryCancelWait()
    {
        if (_cts != null)
        {
            // If it has already been executed, cancel it and wait until the preceding process is finished.
            // (It waits because it has a lock)
            _cts.Cancel();
            lock (_locker)
            {
                // dispose after it is no longer used.
                _cts.Dispose();
                _cts = null;
            }
        }
    }

    public void Clear()
    {
        lock (_subsLocker)
        {
            CurrentIndex = -1;
            SelectedSub = null;
            foreach (var sub in Subs)
            {
                sub.Dispose();
            }
            Subs.Clear();
            State = PositionState.First;
            LanguageSource = null;
            IsLoading = false;
            Width = 0;
            Height = 0;
        }
    }

    public void Reset()
    {
        TryCancelWait();
        Clear();
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    #endregion
}

public unsafe class SubtitleReader : IDisposable
{
    private readonly SubManager _manager;
    private readonly Config _config;
    private readonly LogHandler Log;
    private readonly int _subIndex;

    private Demuxer? _demuxer;
    private SubtitlesDecoder? _decoder;
    private SubtitlesStream? _stream;

    private AVPacket* _packet = null;

    public SubtitleReader(SubManager manager, Config config, int subIndex)
    {
        _manager = manager;
        _config = config;
        Log = new LogHandler(("[#1]").PadRight(8, ' ') + $" [SubReader{subIndex + 1}    ] ");

        _subIndex = subIndex;
    }

    public void Open(string url, int streamIndex, MediaType type, CancellationToken token)
    {
        _demuxer = new Demuxer(_config.Demuxer, type, _subIndex + 1, false);

        token.Register(() =>
        {
            if (_demuxer != null)
                _demuxer.Interrupter.ForceInterrupt = 1;
        });

        _demuxer.Log.Prefix = _demuxer.Log.Prefix.Replace("Demuxer: ", "DemuxerS:");
        string? error = _demuxer.Open(url);

        if (error != null)
        {
            token.ThrowIfCancellationRequested(); // if canceled

            throw new InvalidOperationException($"demuxer open error: {error}");
        }

        _stream = (SubtitlesStream)_demuxer.AVStreamToStream[streamIndex];

        if (type == MediaType.Subs)
        {

            _stream.ExternalStream = new ExternalSubtitlesStream()
            {
                Url = url,
                IsBitmap = _stream.IsBitmap
            };

            _stream.ExternalStreamAdded();
        }

        _decoder = new SubtitlesDecoder(_config, _subIndex + 1);
        _decoder.Log.Prefix = _decoder.Log.Prefix.Replace("Decoder: ", "DecoderS:");
        error = _decoder.Open(_stream);

        if (error != null)
        {
            token.ThrowIfCancellationRequested(); // if canceled

            throw new InvalidOperationException($"decoder open error: {error}");
        }
    }

    /// <summary>
    /// Read subtitle stream to the end and get all subtitle data
    /// </summary>
    /// <param name="useBitmap"></param>
    /// <param name="addSub"></param>
    /// <param name="token"></param>
    /// <exception cref="OperationCanceledException">The token has had cancellation requested.</exception>
    public void ReadAll(bool useBitmap, Action<SubtitleData> addSub, CancellationToken token)
    {
        if (_demuxer == null || _decoder == null || _stream == null)
            throw new InvalidOperationException("Open() is not called");

        SubtitleData? prevSub = null;

        _packet = av_packet_alloc();

        int demuxErrors = 0;
        int decodeErrors = 0;

        while (!token.IsCancellationRequested)
        {
            _demuxer.Interrupter.ReadRequest();
            int ret = av_read_frame(_demuxer.fmtCtx, _packet);

            if (ret != 0)
            {
                av_packet_unref(_packet);

                if (_demuxer.Interrupter.Timedout)
                {
                    if (token.IsCancellationRequested)
                        break;

                    ret.ThrowExceptionIfError("av_read_frame (timed out)");
                }

                if (ret == AVERROR_EOF || token.IsCancellationRequested)
                {
                    break;
                }

                // demux error
                if (CanWarn) Log.Warn($"av_read_frame: {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                if (++demuxErrors == _config.Demuxer.MaxErrors)
                {
                    ret.ThrowExceptionIfError("av_read_frame");
                }

                continue;
            }

            // Discard all but the subtitle stream.
            if (_packet->stream_index != _stream.StreamIndex)
            {
                av_packet_unref(_packet);
                continue;
            }

            SubtitleData subData = new();
            int gotSub = 0;
            AVSubtitle sub = default;

            ret = avcodec_decode_subtitle2(_decoder.CodecCtx, &sub, &gotSub, _packet);
            if (ret < 0)
            {
                // decode error
                av_packet_unref(_packet);
                if (CanWarn) Log.Warn($"avcodec_decode_subtitle2: {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");
                if (++decodeErrors == _config.Decoder.MaxErrors)
                {
                    ret.ThrowExceptionIfError("avcodec_decode_subtitle2");
                }

                continue;
            }

            if (gotSub == 0)
            {
                av_packet_unref(_packet);
                continue;
            }

            long pts = AV_NOPTS_VALUE; // 0.1us
            if (sub.pts != AV_NOPTS_VALUE)
            {
                pts = sub.pts /*us*/ * 10;
            }
            else if (_packet->pts != AV_NOPTS_VALUE)
            {
                pts = (long)(_packet->pts * _stream.Timebase);
            }

            av_packet_unref(_packet);

            if (pts == AV_NOPTS_VALUE)
            {
                continue;
            }

            if (_stream.IsBitmap)
            {
                // Cache the width and height of the video for use in displaying bitmap subtitles
                // width and height may be 0 unless after decoding the subtitles
                // In this case, bitmap subtitles cannot be displayed correctly, so the size should be cached here
                if (_manager.Width != _decoder.CodecCtx->width)
                    _manager.Width = _decoder.CodecCtx->width;
                if (_manager.Height != _decoder.CodecCtx->height)
                    _manager.Height = _decoder.CodecCtx->height;
            }

            // Bitmap PGS has a special format.
            if (_stream.IsBitmap && prevSub != null
                /*&& _stream->codecpar->codec_id == AVCodecID.AV_CODEC_ID_HDMV_PGS_SUBTITLE*/)
            {
                if (sub.num_rects < 1)
                {
                    // Support for special format bitmap subtitles.
                    // In the case of bitmap subtitles, num_rects = 0 and 1 may alternate.
                    // In this case sub->start_display_time and sub->end_display_time are always fixed at 0 and
                    // AVPacket->duration is also always 0.
                    // This indicates the end of the previous subtitle, and the time in pts is the end time of the previous subtitle.

                    // Note that not all bitmap subtitles have this behavior.

                    // Assign pts as the end time of the previous subtitle
                    prevSub.EndTime = new TimeSpan(pts - _demuxer.StartTime);
                    addSub(prevSub);
                    prevSub = null;

                    avsubtitle_free(&sub);
                    continue;
                }

                // There are cases where num_rects = 1 is consecutive.
                // In this case, the previous subtitle end time is corrected by pts, and a new subtitle is started with the same pts.
                if (prevSub.Bitmap?.Sub.end_display_time == uint.MaxValue) // 4294967295
                {
                    prevSub.EndTime = new TimeSpan(pts - _demuxer.StartTime);
                    addSub(prevSub);
                    prevSub = null;
                }
            }

            subData.StartTime = new TimeSpan(pts - _demuxer.StartTime);
            subData.EndTime = subData.StartTime.Add(TimeSpan.FromMilliseconds(sub.end_display_time));

            switch (sub.rects[0]->type)
            {
                case AVSubtitleType.Text:
                    subData.Text = Utils.BytePtrToStringUTF8(sub.rects[0]->text).Trim();
                    avsubtitle_free(&sub);

                    if (string.IsNullOrEmpty(subData.Text))
                    {
                        continue;
                    }

                    break;
                case AVSubtitleType.Ass:
                    string text = Utils.BytePtrToStringUTF8(sub.rects[0]->ass).Trim();
                    avsubtitle_free(&sub);

                    subData.Text = ParseSubtitles.SSAtoSubStyles(text, out var subStyles).Trim();
                    subData.SubStyles = subStyles;

                    if (string.IsNullOrEmpty(subData.Text))
                    {
                        continue;
                    }

                    break;

                case AVSubtitleType.Bitmap:
                    subData.IsBitmap = true;

                    if (useBitmap)
                    {
                        // Save subtitle data for (OCR or subtitle cache)
                        subData.Bitmap = new SubtitleBitmapData(sub);
                    }
                    else
                    {
                        // Only subtitle timestamp information is used, so bitmap is released
                        avsubtitle_free(&sub);
                    }

                    break;
            }

            if (prevSub != null)
            {
                addSub(prevSub);
            }

            prevSub = subData;
        }

        if (token.IsCancellationRequested)
        {
            prevSub?.Dispose();
            token.ThrowIfCancellationRequested();
        }

        // Process last
        if (prevSub != null)
        {
            addSub(prevSub);
        }
    }

    private bool _isDisposed;
    public void Dispose()
    {
        if (_isDisposed)
            return;

        // av_packet_alloc
        if (_packet != null)
        {
            fixed (AVPacket** ptr = &_packet)
            {
                av_packet_free(ptr);
            }
        }

        _decoder?.Dispose();
        if (_demuxer != null)
        {
            _demuxer.Interrupter.ForceInterrupt = 0;
            _demuxer.Dispose();
        }

        _isDisposed = true;
    }
}

public class SubtitleBitmapData : IDisposable
{
    public SubtitleBitmapData(AVSubtitle sub)
    {
        Sub = sub;
    }

    private readonly ReaderWriterLockSlim _rwLock = new();
    private bool _isDisposed;

    public AVSubtitle Sub;

    public WriteableBitmap SubToWritableBitmap(bool isGrey)
    {
        (byte[] data, AVSubtitleRect rect) = SubToBitmap(isGrey);

        WriteableBitmap wb = new(
            rect.w, rect.h,
            Utils.NativeMethods.DpiXSource, Utils.NativeMethods.DpiYSource,
            PixelFormats.Bgra32, null
        );
        Int32Rect dirtyRect = new(0, 0, rect.w, rect.h);
        wb.Lock();

        Marshal.Copy(data, 0, wb.BackBuffer, data.Length);

        wb.AddDirtyRect(dirtyRect);
        wb.Unlock();
        wb.Freeze();

        return wb;
    }

    public unsafe (byte[] data, AVSubtitleRect rect) SubToBitmap(bool isGrey)
    {
        if (_isDisposed)
            throw new InvalidOperationException("already disposed");

        try
        {
            // Prevent from disposing
            _rwLock.EnterReadLock();

            AVSubtitleRect rect = *Sub.rects[0];
            byte[] data = Renderer.ConvertBitmapSub(Sub, isGrey);

            return (data, rect);
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }

    public void Dispose()
    {
        if (_isDisposed)
            return;

        _rwLock.EnterWriteLock();

        if (Sub.num_rects > 0)
        {
            unsafe
            {
                fixed (AVSubtitle* subPtr = &Sub)
                {
                    avsubtitle_free(subPtr);
                }
            }
        }

        _isDisposed = true;
        _rwLock.ExitWriteLock();

#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

#if DEBUG
    ~SubtitleBitmapData()
    {
        System.Diagnostics.Debug.Fail("Dispose is not called");
    }
#endif
}

public class SubtitleData : IDisposable, INotifyPropertyChanged
{
    public int Index { get; set; }

    public string? Text
    {
        get;
        set
        {
            var prevIsText = IsText;
            if (Set(ref field, value))
            {
                if (prevIsText != IsText)
                    OnPropertyChanged(nameof(IsText));
                OnPropertyChanged(nameof(DisplayText));
            }
        }
    }

    public string? TranslatedText
    {
        get;
        set
        {
            var prevUseTranslated = UseTranslated;
            if (Set(ref field, value))
            {
                if (prevUseTranslated != UseTranslated)
                {
                    OnPropertyChanged(nameof(UseTranslated));
                }
                OnPropertyChanged(nameof(DisplayText));
            }
        }
    }

    public bool IsText => !string.IsNullOrEmpty(Text);

    public bool IsTranslated => TranslatedText != null;
    public bool UseTranslated => EnabledTranslated && IsTranslated;

    public bool EnabledTranslated = true;

    public string? DisplayText => UseTranslated ? TranslatedText : Text;

    public List<SubStyle>? SubStyles;
    public TimeSpan StartTime { get; set; }
    public TimeSpan EndTime { get; set; }
#if DEBUG
    public int ChunkNo { get; set => Set(ref field, value); }
    public TimeSpan StartTimeChunk { get; set => Set(ref field, value); }
    public TimeSpan EndTimeChunk { get; set => Set(ref field, value); }
#endif
    public TimeSpan Duration => EndTime - StartTime;

    public SubtitleBitmapData? Bitmap { get; set; }

    public bool IsBitmap { get; set; }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed)
            return;

        if (IsBitmap && Bitmap != null)
        {
            Bitmap.Dispose();
            Bitmap = null;
        }

        _isDisposed = true;
    }

    public SubtitleData Clone()
    {
        return new SubtitleData()
        {
            Index = Index,
            Text = Text,
            TranslatedText = TranslatedText,
            EnabledTranslated = EnabledTranslated,
            StartTime = StartTime,
            EndTime = EndTime,
#if DEBUG
            ChunkNo = ChunkNo,
            StartTimeChunk = StartTimeChunk,
            EndTimeChunk = EndTimeChunk,
#endif
            IsBitmap = IsBitmap,
            Bitmap = null,
        };
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    #endregion
}

public class SubtitleTimeStartComparer : Comparer<SubtitleData>
{
    public static SubtitleTimeStartComparer Instance { get; } = new();
    private SubtitleTimeStartComparer() { }
    static SubtitleTimeStartComparer() { }

    public override int Compare(SubtitleData? x, SubtitleData? y)
    {
        if (object.Equals(x, y)) return 0;
        if (x == null) return -1;
        if (y == null) return 1;

        return x.StartTime.CompareTo(y.StartTime);
    }
}

public class SubtitleTimeEndComparer : Comparer<SubtitleData>
{
    public override int Compare(SubtitleData? x, SubtitleData? y)
    {
        if (object.Equals(x, y)) return 0;
        if (x == null) return -1;
        if (y == null) return 1;

        return x.EndTime.CompareTo(y.EndTime);
    }
}

internal static class WrapperHelper
{
    public static int ThrowExceptionIfError(this int error, string message)
    {
        if (error < 0)
        {
            string errStr = AvErrorStr(error);
            throw new InvalidOperationException($"{message}: {errStr} ({error})");
        }

        return error;
    }

    public static unsafe string AvErrorStr(this int error)
    {
        int bufSize = 1024;
        byte* buf = stackalloc byte[bufSize];

        if (av_strerror(error, buf, (nuint)bufSize) == 0)
        {
            string errStr = Marshal.PtrToStringAnsi((IntPtr)buf)!;
            return errStr;
        }

        return "unknown error";
    }
}

public static class ObservableCollectionExtensions
{
    public static int FindIndex<T>(this ObservableCollection<T> collection, Predicate<T> match)
    {
        ArgumentNullException.ThrowIfNull(collection);
        ArgumentNullException.ThrowIfNull(match);

        for (int i = 0; i < collection.Count; i++)
        {
            if (match(collection[i]))
                return i;
        }
        return -1;
    }

    public static int BinarySearch<T>(this ObservableCollection<T> collection, T item, IComparer<T> comparer)
    {
        ArgumentNullException.ThrowIfNull(collection);

        //comparer ??= Comparer<T>.Default;
        int low = 0;
        int high = collection.Count - 1;

        while (low <= high)
        {
            int mid = low + ((high - low) / 2);
            int comparison = comparer.Compare(collection[mid], item);

            if (comparison == 0)
                return mid;
            if (comparison < 0)
                low = mid + 1;
            else
                high = mid - 1;
        }

        return ~low;
    }

    public static IEnumerable<T> GetRange<T>(this ObservableCollection<T> collection, int index, int count)
    {
        ArgumentNullException.ThrowIfNull(collection);
        if (index < 0 || count < 0 || (index + count) > collection.Count)
            throw new ArgumentOutOfRangeException();

        return collection.Skip(index).Take(count);
    }

    public static void Sort<T>(this ObservableCollection<T> collection, IComparer<T> comparer)
    {
        ArgumentNullException.ThrowIfNull(collection);
        ArgumentNullException.ThrowIfNull(comparer);

        List<T> sortedList = collection.ToList();
        sortedList.Sort(comparer);

        collection.Clear();
        foreach (var item in sortedList)
        {
            collection.Add(item);
        }
    }
}

public class BulkObservableCollection<T> : ObservableCollection<T>
{
    private bool _suppressNotification;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }

    public void AddRange(IEnumerable<T> list)
    {
        ArgumentNullException.ThrowIfNull(list);

        _suppressNotification = true;

        foreach (T item in list)
        {
            Add(item);
        }
        _suppressNotification = false;

        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

using System;
using System.Collections.ObjectModel;

using Vortice.Multimedia;
using Vortice.XAudio2;

using static Vortice.XAudio2.XAudio2;

using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaStream;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

public class Audio : NotifyPropertyChanged
{
    public event EventHandler<AudioFrame> SamplesAdded;

    #region Properties
    /// <summary>
    /// Embedded Streams
    /// </summary>
    public ObservableCollection<AudioStream>
                    Streams         => decoder?.VideoDemuxer.AudioStreams; // TBR: We miss AudioDemuxer embedded streams

    /// <summary>
    /// Whether the input has audio and it is configured
    /// </summary>
    public bool     IsOpened        { get => isOpened;          internal set => Set(ref _IsOpened, value); }
    internal bool   _IsOpened, isOpened;

    public string   Codec           { get => codec;             internal set => Set(ref _Codec, value); }
    internal string _Codec, codec;

    ///// <summary>
    ///// Audio bitrate (Kbps)
    ///// </summary>
    public double   BitRate         { get => bitRate;           internal set => Set(ref _BitRate, value); }
    internal double _BitRate, bitRate;

    public int      Bits            { get => bits;              internal set => Set(ref _Bits, value); }
    internal int    _Bits, bits;

    public int      Channels        { get => channels;          internal set => Set(ref _Channels, value); }
    internal int    _Channels, channels;

    /// <summary>
    /// Audio player's channels out (currently 2 channels supported only)
    /// </summary>
    public int      ChannelsOut     { get; } = 2;

    public string   ChannelLayout   { get => channelLayout;     internal set => Set(ref _ChannelLayout, value); }
    internal string _ChannelLayout, channelLayout;

    ///// <summary>
    ///// Total Dropped Frames
    ///// </summary>
    public int      FramesDropped   { get => framesDropped;     internal set => Set(ref _FramesDropped, value); }
    internal int    _FramesDropped, framesDropped;

    public int      FramesDisplayed { get => framesDisplayed;   internal set => Set(ref _FramesDisplayed, value); }
    internal int    _FramesDisplayed, framesDisplayed;

    public string   SampleFormat    { get => sampleFormat;      internal set => Set(ref _SampleFormat, value); }
    internal string _SampleFormat, sampleFormat;

    /// <summary>
    /// Audio sample rate (in/out)
    /// </summary>
    public int      SampleRate      { get => sampleRate;        internal set => Set(ref _SampleRate, value); }
    internal int    _SampleRate, sampleRate;

    /// <summary>
    /// Audio player's volume / amplifier (valid values 0 - no upper limit)
    /// </summary>
    public int Volume
    {
        get
        {
            lock (locker)
                return sourceVoice == null || Mute ? _Volume : (int) ((decimal)sourceVoice.Volume * 100);
        }
        set
        {
            if (value > Config.Player.VolumeMax || value < 0)
                return;

            if (value == 0)
                Mute = true;
            else if (Mute)
            {
                _Volume = value;
                Mute = false;
            }
            else
            {
                if (sourceVoice != null)
                    sourceVoice.Volume = Math.Max(0, value / 100.0f);
            }

            Set(ref _Volume, value, false);
        }
    }
    int _Volume;

    /// <summary>
    /// Audio player's mute
    /// </summary>
    public bool Mute
    {
        get => mute;
        set
        {
            lock (locker)
            {
                if (sourceVoice == null)
                    return;

                sourceVoice.Volume = value ? 0 : _Volume / 100.0f;
            }

            Set(ref mute, value, false);
        }
    }
    private bool mute = false;

    /// <summary>
    /// <para>Audio player's current device (available devices can be found on <see cref="Engine.Audio"/>)/></para>
    /// </summary>
    public AudioEngine.AudioEndpoint Device
    {
        get => _Device;
        set
        {
            if ((value == null && _Device == Engine.Audio.DefaultDevice) || value == _Device)
                return;

            _Device = value ?? Engine.Audio.DefaultDevice;
            Initialize();
            RaiseUI(nameof(Device));
        }
    }
    internal AudioEngine.AudioEndpoint _Device = Engine.Audio.DefaultDevice;
    #endregion

    #region Declaration
    public Player Player => player;

    Player                  player;
    Config                  Config => player.Config;
    DecoderContext          decoder => player?.decoder;

    Action                  uiAction;
    internal readonly object
                            locker = new();

    IXAudio2                xaudio2;
    internal IXAudio2MasteringVoice
                            masteringVoice;
    internal IXAudio2SourceVoice
                            sourceVoice;
    WaveFormat              waveFormat  = new(48000, 16, 2); // Output Audio Device
    AudioBuffer             audioBuffer = new();
    internal double         Timebase;
    internal ulong          submittedSamples;
    #endregion

    public Audio(Player player)
    {
        this.player = player;

        uiAction = () =>
        {
            IsOpened        = IsOpened;
            Codec           = Codec;
            BitRate         = BitRate;
            Bits            = Bits;
            Channels        = Channels;
            ChannelLayout   = ChannelLayout;
            SampleFormat    = SampleFormat;
            SampleRate      = SampleRate;

            FramesDisplayed = FramesDisplayed;
            FramesDropped   = FramesDropped;
        };

        // Set default volume
        Volume = Math.Min(Config.Player.VolumeDefault, Config.Player.VolumeMax);
        Initialize();
    }

    internal void Initialize()
    {
        lock (locker)
        {
            if (Engine.Audio.Failed)
            {
                Config.Audio.Enabled = false;
                return;
            }

            sampleRate = decoder != null && decoder.AudioStream != null && decoder.AudioStream.SampleRate > 0 ? decoder.AudioStream.SampleRate : 48000;
            player.Log.Info($"Initialiazing audio ({Device.Name} - {Device.Id} @ {SampleRate}Hz)");

            Dispose();

            try
            {
                xaudio2 = XAudio2Create();

                try
                {
                    masteringVoice = xaudio2.CreateMasteringVoice(0, 0, AudioStreamCategory.GameEffects, _Device == Engine.Audio.DefaultDevice ? null : _Device.Id);
                }
                catch (Exception) // Win 7/8 compatibility issue https://social.msdn.microsoft.com/Forums/en-US/4989237b-814c-4a7a-8a35-00714d36b327/xaudio2-how-to-get-device-id-for-mastering-voice?forum=windowspro-audiodevelopment
                {
                    masteringVoice = xaudio2.CreateMasteringVoice(0, 0, AudioStreamCategory.GameEffects, _Device == Engine.Audio.DefaultDevice ? null : (@"\\?\swd#mmdevapi#" + _Device.Id.ToLower() + @"#{e6327cad-dcec-4949-ae8a-991e976a79d2}"));
                }

                sourceVoice = xaudio2.CreateSourceVoice(waveFormat, false);
                sourceVoice.SetSourceSampleRate((uint)SampleRate);
                sourceVoice.Start();

                submittedSamples        = 0;
                Timebase                = 1000 * 10000.0 / sampleRate;
                masteringVoice.Volume   = Config.Player.VolumeMax / 100.0f;
                sourceVoice.Volume      = mute ? 0 : Math.Max(0, _Volume / 100.0f);
            }
            catch (Exception e)
            {
                player.Log.Info($"Audio initialization failed ({e.Message})");
                Config.Audio.Enabled = false;
            }
        }
    }
    internal void Dispose()
    {
        lock (locker)
        {
            if (xaudio2 == null)
                return;

            xaudio2.        Dispose();
            sourceVoice?.   Dispose();
            masteringVoice?.Dispose();
            xaudio2         = null;
            sourceVoice     = null;
            masteringVoice  = null;
        }
    }

    // TBR: Very rarely could crash the app on audio device change while playing? Requires two locks (Audio's locker and aFrame)
    // The process was terminated due to an internal error in the .NET Runtime at IP 00007FFA6725DA03 (00007FFA67090000) with exit code c0000005.
    [System.Security.SecurityCritical]
    internal void AddSamples(AudioFrame aFrame)
    {
        lock (locker) // required for submittedSamples only? (ClearBuffer() can be called during audio decocder circular buffer reallocation)
        {
            try
            {
                if (CanTrace)
                    player.Log.Trace($"[A] Presenting {Utils.TicksToTime(player.aFrame.timestamp)}");

                framesDisplayed++;

                submittedSamples += (ulong) (aFrame.dataLen / 4); // ASampleBytes
                SamplesAdded?.Invoke(this, aFrame);

                audioBuffer.AudioDataPointer= aFrame.dataPtr;
                audioBuffer.AudioBytes      = (uint)aFrame.dataLen;
                sourceVoice.SubmitSourceBuffer(audioBuffer);
            }
            catch (Exception e) // Happens on audio device changed/removed
            {
                if (CanDebug)
                    player.Log.Debug($"[Audio] Submitting samples failed ({e.Message})");

                ClearBuffer(); // TBR: Inform player to resync audio?
            }
        }
    }
    internal long GetBufferedDuration() { lock (locker) { return (long) ((submittedSamples - sourceVoice.State.SamplesPlayed) * Timebase); } }
    internal long GetDeviceDelay()      { lock (locker) { return (long) ((xaudio2.PerformanceData.CurrentLatencyInSamples * Timebase) - 80000); } } // TODO: VBlack delay (8ms correction for now)
    internal void ClearBuffer()
    {
        lock (locker)
        {
            if (sourceVoice == null)
                return;

            sourceVoice.Stop();
            sourceVoice.FlushSourceBuffers();
            sourceVoice.Start();
            submittedSamples = sourceVoice.State.SamplesPlayed;
        }
    }

    internal void Reset()
    {
        codec           = null;
        bitRate         = 0;
        bits            = 0;
        channels        = 0;
        channelLayout   = null;
        sampleFormat    = null;
        isOpened        = false;

        ClearBuffer();
        player.UIAdd(uiAction);
    }
    internal void Refresh()
    {
        if (decoder.AudioStream == null) { Reset(); return; }

        codec           = decoder.AudioStream.Codec;
        bits            = decoder.AudioStream.Bits;
        channels        = decoder.AudioStream.Channels;
        channelLayout   = decoder.AudioStream.ChannelLayoutStr;
        sampleFormat    = decoder.AudioStream.SampleFormatStr;
        isOpened        =!decoder.AudioDecoder.Disposed;

        framesDisplayed = 0;
        framesDropped   = 0;

        if (SampleRate!= decoder.AudioStream.SampleRate)
            Initialize();

        player.UIAdd(uiAction);
    }
    internal void Enable()
    {
        bool wasPlaying = player.IsPlaying;

        decoder.OpenSuggestedAudio();

        player.ReSync(decoder.AudioStream, (int) (player.CurTime / 10000), true);

        Refresh();
        player.UIAll();

        if (wasPlaying || Config.Player.AutoPlay)
            player.Play();
    }
    internal void Disable()
    {
        if (!IsOpened)
            return;

        decoder.CloseAudio();

        player.aFrame = null;

        if (!player.Video.IsOpened)
        {
            player.canPlay = false;
            player.UIAdd(() => player.CanPlay = player.CanPlay);
        }

        Reset();
        player.UIAll();
    }

    public void DelayAdd()      => Config.Audio.Delay += Config.Player.AudioDelayOffset;
    public void DelayAdd2()     => Config.Audio.Delay += Config.Player.AudioDelayOffset2;
    public void DelayRemove()   => Config.Audio.Delay -= Config.Player.AudioDelayOffset;
    public void DelayRemove2()  => Config.Audio.Delay -= Config.Player.AudioDelayOffset2;
    public void Toggle()        => Config.Audio.Enabled = !Config.Audio.Enabled;
    public void ToggleMute()    => Mute = !Mute;
    public void VolumeUp()
    {
        if (Volume == Config.Player.VolumeMax) return;
        Volume = Math.Min(Volume + Config.Player.VolumeOffset, Config.Player.VolumeMax);
    }
    public void VolumeDown()
    {
        if (Volume == 0) return;
        Volume = Math.Max(Volume - Config.Player.VolumeOffset, 0);
    }

    /// <summary>
    /// Reloads filters from Config.Audio.Filters (experimental)
    /// </summary>
    /// <returns>0 on success</returns>
    public int ReloadFilters() => player.AudioDecoder.ReloadFilters();

    /// <summary>
    /// <para>
    /// Updates filter's property (experimental)
    /// Note: This will not update the property value in Config.Audio.Filters
    /// </para>
    /// </summary>
    /// <param name="filterId">Filter's unique id specified in Config.Audio.Filters</param>
    /// <param name="key">Filter's property to change</param>
    /// <param name="value">Filter's property value</param>
    /// <returns>0 on success</returns>
    public int UpdateFilter(string filterId, string key, string value) => player.AudioDecoder.UpdateFilter(filterId, key, value);
}

using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shell;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using InputType = FlyleafLib.InputType;

namespace LLPlayer.ViewModels;

public class MainWindowVM : Bindable
{
    public FlyleafManager FL { get; }
    private readonly LogHandler Log;

    public MainWindowVM(FlyleafManager fl)
    {
        FL = fl;
        Log = new LogHandler("[App] [MainWindowVM  ] ");
    }

    public string Title { get; set => Set(ref field, value); } = App.Name;

    #region Progress in TaskBar
    public double TaskBarProgressValue
    {
        get;
        set
        {
            double v = value;
            if (v < 0.01)
            {
                // Set to 1% because it is not displayed.
                v = 0.01;
            }
            Set(ref field, v);
        }
    }

    public TaskbarItemProgressState TaskBarProgressState { get; set => Set(ref field, value); }
    #endregion

    #region Action Button in TaskBar
    public Visibility PlayPauseVisibility { get; set => Set(ref field, value); } = Visibility.Collapsed;
    public ImageSource PlayPauseImageSource { get; set => Set(ref field, value); } = PlayIcon;

    private static readonly BitmapImage PlayIcon = new(
        new Uri("pack://application:,,,/Resources/Images/play.png"));

    private static readonly BitmapImage PauseIcon = new(
        new Uri("pack://application:,,,/Resources/Images/pause.png"));
    #endregion

    public DelegateCommand? CmdOnLoaded => field ??= new(() =>
    {
        // error handling
        FL.Player.KnownErrorOccurred += (sender, args) =>
        {
            Utils.UI(() =>
            {
                Log.Error($"Known error occurred in Flyleaf: {args.Message} ({args.ErrorType.ToString()})");
                ErrorDialogHelper.ShowKnownErrorPopup(args.Message, args.ErrorType);
            });
        };

        FL.Player.UnknownErrorOccurred += (sender, args) =>
        {
            Utils.UI(() =>
            {
                Log.Error($"Unknown error occurred in Flyleaf: {args.Message}: {args.Exception}");
                ErrorDialogHelper.ShowUnknownErrorPopup(args.Message, args.ErrorType, args.Exception);
            });
        };

        FL.Player.PropertyChanged += (sender, args) =>
        {
            switch (args.PropertyName)
            {
                case nameof(FL.Player.CurTime):
                {
                    double prevValue = TaskBarProgressValue;
                    double newValue = (double)FL.Player.CurTime / FL.Player.Duration;

                    if (Math.Abs(newValue - prevValue) >= 0.01) // prevent frequent update
                    {
                        TaskBarProgressValue = newValue;
                    }

                    break;
                }
                case nameof(FL.Player.Status):
                    // Progress in TaskBar (and title)
                    switch (FL.Player.Status)
                    {
                        case Status.Stopped:
                            // reset
                            Title = App.Name;
                            TaskBarProgressState = TaskbarItemProgressState.None;
                            TaskBarProgressValue = 0;
                            break;
                        case Status.Playing:
                            TaskBarProgressState = TaskbarItemProgressState.Normal;
                            break;
                        case Status.Opening:
                            TaskBarProgressState = TaskbarItemProgressState.Indeterminate;
                            TaskBarProgressValue = 0;
                            break;
                        case Status.Paused:
                            TaskBarProgressState = TaskbarItemProgressState.Paused;
                            break;
                        case Status.Ended:
                            TaskBarProgressState = TaskbarItemProgressState.Paused;
                            TaskBarProgressValue = 1;
                            break;
                        case Status.Failed:
                            TaskBarProgressState = TaskbarItemProgressState.Error;
                            break;
                    }

                    // Action Button in TaskBar
                    switch (FL.Player.Status)
                    {
                        case Status.Paused:
                        case Status.Playing:
                            PlayPauseVisibility = Visibility.Visible;
                            PlayPauseImageSource = FL.Player.Status == Status.Playing ? PauseIcon : PlayIcon;
                            break;
                        default:
                            PlayPauseVisibility = Visibility.Collapsed;
                            break;
                    }

                    break;
            }
        };

        FL.Player.OpenCompleted += (sender, args) =>
        {
            if (!args.Success || args.IsSubtitles)
            {
                return;
            }

            string name = Path.GetFileName(args.Url);
            if (FL.Player.Playlist.InputType == InputType.Web)
            {
                name = FL.Player.Playlist.Selected.Title;
            }
            Title = $"{name} - {App.Name}";
            TaskBarProgressValue = 0;
            TaskBarProgressState = TaskbarItemProgressState.Normal;
        };

        if (App.CmdUrl != null)
        {
            FL.Player.OpenAsync(App.CmdUrl);
        }
    });

    public DelegateCommand? CmdOnClosing => field ??= new(() =>
    {
        FL.Player.Dispose();
    });
}

using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Vortice.MediaFoundation;

namespace FlyleafLib.MediaFramework.MediaDevice;

public class VideoDeviceStream : DeviceStreamBase
{
    public string   MajorType           { get; }
    public string   SubType             { get; }
    public int      FrameSizeWidth      { get; }
    public int      FrameSizeHeight     { get; }
    public int      FrameRate           { get; }
    private string  FFmpegFormat        { get; }

    public VideoDeviceStream(string deviceName, Guid majorType, Guid subType, int frameSizeWidth, int frameSizeHeight, int frameRate, int frameRateDenominator) : base(deviceName)
    {
        MajorType           = MediaTypeGuids.Video == majorType ? "Video" : "Unknown";
        SubType             = GetPropertyName(subType);
        FrameSizeWidth      = frameSizeWidth;
        FrameSizeHeight     = frameSizeHeight;
        FrameRate           = frameRate / frameRateDenominator;
        FFmpegFormat        = GetFFmpegFormat(SubType);
        Url                 = $"fmt://dshow?video={DeviceFriendlyName}&video_size={FrameSizeWidth}x{FrameSizeHeight}&framerate={FrameRate}&{FFmpegFormat}";
    }

    private static string GetPropertyName(Guid guid)
    {
        var type = typeof(VideoFormatGuids);
        foreach (var property in type.GetFields(BindingFlags.Public | BindingFlags.Static))
            if (property.FieldType == typeof(Guid))
            {
                object temp = property.GetValue(null);
                if (temp is Guid value)
                    if (value == guid)
                        return property.Name.ToUpper();
            }

        return null;
    }
    private static unsafe string GetFFmpegFormat(string subType)
    {
        switch (subType)
        {
            case "MJPG":
                var descriptorPtr = avcodec_descriptor_get(AVCodecID.Mjpeg);
                return $"vcodec={Utils.BytePtrToStringUTF8(descriptorPtr->name)}";
            case "YUY2":
                return $"pixel_format={av_get_pix_fmt_name(AVPixelFormat.Yuyv422)}";
            case "NV12":
                return $"pixel_format={av_get_pix_fmt_name(AVPixelFormat.Nv12)}";
            default:
                return "";
        }
    }
    public override string ToString() => $"{SubType}, {FrameSizeWidth}x{FrameSizeHeight}, {FrameRate}FPS";

    #region VideoFormatsViaMediaSource
    public static IList<VideoDeviceStream> GetVideoFormatsForVideoDevice(string friendlyName, string symbolicLink)
    {
        List<VideoDeviceStream> formatList = new();

        using (var mediaSource = GetMediaSourceFromVideoDevice(symbolicLink))
        {
            if (mediaSource == null)
                return null;

            using var sourcePresentationDescriptor = mediaSource.CreatePresentationDescriptor();
            int sourceStreamCount = sourcePresentationDescriptor.StreamDescriptorCount;

            for (int i = 0; i < sourceStreamCount; i++)
            {
                var guidMajorType = GetMajorMediaTypeFromPresentationDescriptor(sourcePresentationDescriptor, i);
                if (guidMajorType != MediaTypeGuids.Video)
                    continue;

                sourcePresentationDescriptor.GetStreamDescriptorByIndex(i, out var streamIsSelected, out var videoStreamDescriptor);

                using (videoStreamDescriptor)
                {
                    if (streamIsSelected == false)
                        continue;

                    using var typeHandler = videoStreamDescriptor.MediaTypeHandler;
                    int mediaTypeCount = typeHandler.MediaTypeCount;

                    for (int mediaTypeId = 0; mediaTypeId < mediaTypeCount; mediaTypeId++)
                        using (var workingMediaType = typeHandler.GetMediaTypeByIndex(mediaTypeId))
                        {
                            var videoFormat = GetVideoFormatFromMediaType(friendlyName, workingMediaType);

                            if (videoFormat.SubType != "NV12") // NV12 is not playable TODO check support for video formats
                                formatList.Add(videoFormat);
                        }
                }
            }
        }

        return formatList.OrderBy(format => format.SubType).ThenBy(format => format.FrameSizeHeight).ThenBy(format => format.FrameRate).ToList();
    }

    private static IMFMediaSource GetMediaSourceFromVideoDevice(string symbolicLink)
    {
        using var attributeContainer = MediaFactory.MFCreateAttributes(2);
        attributeContainer.Set(CaptureDeviceAttributeKeys.SourceType, CaptureDeviceAttributeKeys.SourceTypeVidcap);
        attributeContainer.Set(CaptureDeviceAttributeKeys.SourceTypeVidcapSymbolicLink, symbolicLink);

        IMFMediaSource ret = null;
        try
        { ret = MediaFactory.MFCreateDeviceSource(attributeContainer); }
        catch (Exception) { }

        return ret;
    }

    private static Guid GetMajorMediaTypeFromPresentationDescriptor(IMFPresentationDescriptor presentationDescriptor, int streamIndex)
    {
        presentationDescriptor.GetStreamDescriptorByIndex(streamIndex, out _, out var streamDescriptor);

        using (streamDescriptor)
            return GetMajorMediaTypeFromStreamDescriptor(streamDescriptor);
    }

    private static Guid GetMajorMediaTypeFromStreamDescriptor(IMFStreamDescriptor streamDescriptor)
    {
        using var pHandler = streamDescriptor.MediaTypeHandler;
        var guidMajorType = pHandler.MajorType;

        return guidMajorType;
    }

    private static VideoDeviceStream GetVideoFormatFromMediaType(string videoDeviceName, IMFMediaType mediaType)
    {
        // MF_MT_MAJOR_TYPE
        // Major type GUID, we return this as human readable text
        var majorType = mediaType.MajorType;

        // MF_MT_SUBTYPE
        // Subtype GUID which describes the basic media type, we return this as human readable text
        var subType = mediaType.GetGUID(MediaTypeAttributeKeys.Subtype);

        // MF_MT_FRAME_SIZE
        // the Width and height of a video frame, in pixels
        MediaFactory.MFGetAttributeSize(mediaType, MediaTypeAttributeKeys.FrameSize, out uint frameSizeWidth, out uint frameSizeHeight);

        // MF_MT_FRAME_RATE
        // The frame rate is expressed as a ratio.The upper 32 bits of the attribute value contain the numerator and the lower 32 bits contain the denominator.
        // For example, if the frame rate is 30 frames per second(fps), the ratio is 30 / 1.If the frame rate is 29.97 fps, the ratio is 30,000 / 1001.
        // we report this back to the user as a decimal
        MediaFactory.MFGetAttributeRatio(mediaType, MediaTypeAttributeKeys.FrameRate, out uint frameRate, out uint frameRateDenominator);

        VideoDeviceStream videoFormat = new(videoDeviceName, majorType, subType, (int)frameSizeWidth,
            (int)frameSizeHeight,
            (int)frameRate,
            (int)frameRateDenominator);

        return videoFormat;
    }
    #endregion
}

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

using Vortice.DXGI;

using FlyleafLib.MediaFramework.MediaDevice;

namespace FlyleafLib;

public class VideoEngine
{
    /// <summary>
    /// List of Video Capture Devices
    /// </summary>
    public ObservableCollection<VideoDevice>
                            CapDevices          { get; set; } = new();
    public void             RefreshCapDevices() => VideoDevice.RefreshDevices();

    /// <summary>
    /// List of GPU Adpaters <see cref="Config.VideoConfig.GPUAdapter"/>
    /// </summary>
    public Dictionary<long, GPUAdapter>
                            GPUAdapters         { get; private set; }

    /// <summary>
    /// List of GPU Outputs from default GPU Adapter (Note: will no be updated on screen connect/disconnect)
    /// </summary>
    public List<GPUOutput>  Screens             { get; private set; } = new List<GPUOutput>();

    internal IDXGIFactory2  Factory;

    internal VideoEngine()
    {
        if (DXGI.CreateDXGIFactory1(out Factory).Failure)
            throw new InvalidOperationException("Cannot create IDXGIFactory1");

        GPUAdapters = GetAdapters();
    }

    private Dictionary<long, GPUAdapter> GetAdapters()
    {
        Dictionary<long, GPUAdapter> adapters = new();

        string dump = "";

        for (uint i=0; Factory.EnumAdapters1(i, out var adapter).Success; i++)
        {
            bool hasOutput = false;

            List<GPUOutput> outputs = new();

            int maxHeight = 0;
            for (uint o=0; adapter.EnumOutputs(o, out var output).Success; o++)
            {
                GPUOutput gpout = new()
                {
                    Id        = GPUOutput.GPUOutputIdGenerator++,
                    DeviceName= output.Description.DeviceName,
                    Left      = output.Description.DesktopCoordinates.Left,
                    Top       = output.Description.DesktopCoordinates.Top,
                    Right     = output.Description.DesktopCoordinates.Right,
                    Bottom    = output.Description.DesktopCoordinates.Bottom,
                    IsAttached= output.Description.AttachedToDesktop,
                    Rotation  = (int)output.Description.Rotation
                };

                if (maxHeight < gpout.Height)
                    maxHeight = gpout.Height;

                outputs.Add(gpout);

                if (gpout.IsAttached)
                    hasOutput = true;

                output.Dispose();
            }

            if (Screens.Count == 0 && outputs.Count > 0)
                Screens = outputs;

            adapters[adapter.Description1.Luid] = new GPUAdapter()
            {
                SystemMemory    = adapter.Description1.DedicatedSystemMemory.Value,
                VideoMemory     = adapter.Description1.DedicatedVideoMemory.Value,
                SharedMemory    = adapter.Description1.SharedSystemMemory.Value,
                Vendor          = VendorIdStr(adapter.Description1.VendorId),
                Description     = adapter.Description1.Description,
                Id              = adapter.Description1.DeviceId,
                Luid            = adapter.Description1.Luid,
                MaxHeight       = maxHeight,
                HasOutput       = hasOutput,
                Outputs         = outputs
            };

            dump += $"[#{i+1}] {adapters[adapter.Description1.Luid]}\r\n";

            adapter.Dispose();
        }

        Engine.Log.Info($"GPU Adapters\r\n{dump}");

        return adapters;
    }

    // Use instead System.Windows.Forms.Screen.FromPoint
    public GPUOutput GetScreenFromPosition(int top, int left)
    {
        foreach(var screen in Screens)
        {
            if (top >= screen.Top && top <= screen.Bottom && left >= screen.Left && left <= screen.Right)
                return screen;
        }

        return null;
    }

    private static string VendorIdStr(uint vendorId)
    {
        switch (vendorId)
        {
            case 0x1002:
                return "ATI";
            case 0x10DE:
                return "NVIDIA";
            case 0x1106:
                return "VIA";
            case 0x8086:
                return "Intel";
            case 0x5333:
                return "S3 Graphics";
            case 0x4D4F4351:
                return "Qualcomm";
            default:
                return "Unknown";
        }
    }
}

using System.ComponentModel;
using System.Runtime.InteropServices;

namespace LLPlayer.Extensions;

// ref: https://stackoverflow.com/questions/44205260/net-core-copy-to-clipboard
public static class WindowsClipboard
{
    public static void SetText(string text)
    {
        OpenClipboard();

        EmptyClipboard();
        IntPtr hGlobal = 0;
        try
        {
            int bytes = (text.Length + 1) * 2;
            hGlobal = Marshal.AllocHGlobal(bytes);

            if (hGlobal == 0)
            {
                ThrowWin32();
            }

            IntPtr target = GlobalLock(hGlobal);

            if (target == 0)
            {
                ThrowWin32();
            }

            try
            {
                Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
            }
            finally
            {
                GlobalUnlock(target);
            }

            if (SetClipboardData(cfUnicodeText, hGlobal) == 0)
            {
                ThrowWin32();
            }

            hGlobal = 0;
        }
        finally
        {
            if (hGlobal != 0)
            {
                Marshal.FreeHGlobal(hGlobal);
            }

            CloseClipboard();
        }
    }

    public static void OpenClipboard()
    {
        int num = 10;
        while (true)
        {
            if (OpenClipboard(0))
            {
                break;
            }

            if (--num == 0)
            {
                ThrowWin32();
            }

            Thread.Sleep(20);
        }
    }

    const uint cfUnicodeText = 13;

    static void ThrowWin32()
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GlobalLock(IntPtr hMem);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool GlobalUnlock(IntPtr hMem);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool OpenClipboard(IntPtr hWndNewOwner);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool CloseClipboard();

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetClipboardData(uint uFormat, IntPtr data);

    [DllImport("user32.dll")]
    static extern bool EmptyClipboard();
}

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using FlyleafLib.MediaPlayer;
using LLPlayer.Converters;
using LLPlayer.Extensions;
using LLPlayer.Services;
using MaterialDesignColors.Recommended;
using KeyBinding = FlyleafLib.MediaPlayer.KeyBinding;

namespace LLPlayer.ViewModels;

public class CheatSheetDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public CheatSheetDialogVM(FlyleafManager fl)
    {
        FL = fl;

        List<KeyBinding> keys = FL.PlayerConfig.Player.KeyBindings.Keys.Where(k => k.IsEnabled).ToList();

        HitCount = keys.Count;

        KeyToStringConverter keyConverter = new();

        List<KeyBindingCSGroup> groups = keys.Select(k =>
            {
                KeyBindingCS key = new()
                {
                    Action = k.Action,
                    ActionName = k.Action.ToString(),
                    Alt = k.Alt,
                    Ctrl = k.Ctrl,
                    Shift = k.Shift,
                    Description = k.Action.GetDescription(),
                    Group = k.Action.ToGroup(),
                    Key = k.Key,
                    KeyName = (string)keyConverter.Convert(k.Key, typeof(string), null, CultureInfo.CurrentCulture),
                    ActionInternal = k.ActionInternal,
                };

                if (key.Action == KeyBindingAction.Custom)
                {
                    if (!Enum.TryParse(k.ActionName, out CustomKeyBindingAction customAction))
                    {
                        HitCount -= 1;
                        return null;
                    }

                    key.ActionName = customAction.ToString();
                    key.CustomAction = customAction;
                    key.Description = customAction.GetDescription();
                    key.Group = customAction.ToGroup();
                }

                return key;
            })
            .Where(k => k != null)
            .OrderBy(k => k!.Action)
            .ThenBy(k => k!.CustomAction)
            .GroupBy(k => k!.Group)
            .OrderBy(g => g.Key)
            .Select(g => new KeyBindingCSGroup()
            {
                Group = g.Key,
                KeyBindings = g.ToList()!
            }).ToList();

        KeyBindingGroups = new List<KeyBindingCSGroup>(groups);

        List<ListCollectionView> collectionViews = KeyBindingGroups.Select(g => (ListCollectionView)CollectionViewSource.GetDefaultView(g.KeyBindings))
            .ToList();
        _collectionViews = collectionViews;

        foreach (ListCollectionView view in collectionViews)
        {
            view.Filter = (obj) =>
            {
                if (obj is not KeyBindingCS key)
                {
                    return false;
                }

                if (string.IsNullOrWhiteSpace(SearchText))
                {
                    return true;
                }

                string query = SearchText.Trim();

                bool match = key.Description.Contains(query, StringComparison.OrdinalIgnoreCase);
                if (match)
                {
                    return true;
                }

                return key.Shortcut.Contains(query, StringComparison.OrdinalIgnoreCase);
            };
        }
    }

    public string SearchText
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                _collectionViews.ForEach(v => v.Refresh());

                HitCount = _collectionViews.Sum(v => v.Count);
            }
        }
    } = string.Empty;

    public int HitCount { get; set => Set(ref field, value); }

    private readonly List<ListCollectionView> _collectionViews;
    public List<KeyBindingCSGroup> KeyBindingGroups { get; set; }

    public DelegateCommand<KeyBindingCS>? CmdAction => field ??= new((key) =>
    {
        FL.Player.Activity.ForceFullActive();
        key.ActionInternal.Invoke();
    });

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); } = $"CheatSheet - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 1000;
    public double WindowHeight { get; set => Set(ref field, value); } = 800;

    public bool CanCloseDialog() => true;
    public void OnDialogClosed() { }
    public void OnDialogOpened(IDialogParameters parameters) { }
    public DialogCloseListener RequestClose { get; }
    #endregion IDialogAware
}

public class KeyBindingCS
{
    public required bool Ctrl { get; init; }
    public required bool Shift { get; init; }
    public required bool Alt { get; init; }
    public required Key Key { get; init; }
    public required string KeyName { get; init; }

    [field: AllowNull, MaybeNull]
    public string Shortcut
    {
        get
        {
            if (field == null)
            {
                string modifiers = "";
                if (Ctrl)
                    modifiers += "Ctrl + ";
                if (Alt)
                    modifiers += "Alt + ";
                if (Shift)
                    modifiers += "Shift + ";
                field = $"{modifiers}{KeyName}";
            }

            return field;
        }
    }

    public required KeyBindingAction Action { get; init; }
    public CustomKeyBindingAction? CustomAction { get; set; }
    public required string ActionName { get; set; }
    public required string Description { get; set; }
    public required KeyBindingActionGroup Group { get; set; }

    public required Action ActionInternal { get; init; }
}

public class KeyBindingCSGroup
{
    public required KeyBindingActionGroup Group { get; init; }

    [field: AllowNull, MaybeNull]
    public string GroupName => field ??= Group.ToString();
    public required List<KeyBindingCS> KeyBindings { get; init; }

    public Color GroupColor =>
        Group switch
        {
            KeyBindingActionGroup.Playback => RedSwatch.Red500,
            KeyBindingActionGroup.Player => PinkSwatch.Pink500,
            KeyBindingActionGroup.Audio => PurpleSwatch.Purple500,
            KeyBindingActionGroup.Video => BlueSwatch.Blue500,
            KeyBindingActionGroup.Subtitles => TealSwatch.Teal500,
            KeyBindingActionGroup.SubtitlesPosition => GreenSwatch.Green500,
            KeyBindingActionGroup.Window => LightGreenSwatch.LightGreen500,
            KeyBindingActionGroup.Other => DeepOrangeSwatch.DeepOrange500,
            _ => BrownSwatch.Brown500
        };
}

using System.Runtime.InteropServices;

using FlyleafLib.MediaFramework.MediaDemuxer;

namespace FlyleafLib.MediaFramework.MediaStream;

public unsafe class VideoStream : StreamBase
{
    public AspectRatio                  AspectRatio         { get; set; }
    public ColorRange                   ColorRange          { get; set; }
    public ColorSpace                   ColorSpace          { get; set; }
    public AVColorTransferCharacteristic
                                        ColorTransfer       { get; set; }
    public double                       Rotation            { get; set; }
    public double                       FPS                 { get; set; }
    public long                         FrameDuration       { get ;set; }
    public uint                         Height              { get; set; }
    public bool                         IsRGB               { get; set; }
    public AVComponentDescriptor[]      PixelComps          { get; set; }
    public int                          PixelComp0Depth     { get; set; }
    public AVPixelFormat                PixelFormat         { get; set; }
    public AVPixFmtDescriptor*          PixelFormatDesc     { get; set; }
    public string                       PixelFormatStr      { get; set; }
    public int                          PixelPlanes         { get; set; }
    public bool                         PixelSameDepth      { get; set; }
    public bool                         PixelInterleaved    { get; set; }
    public int                          TotalFrames         { get; set; }
    public uint                         Width               { get; set; }
    public bool                         FixTimestamps       { get; set; } // TBR: For formats such as h264/hevc that have no or invalid pts values

    public override string GetDump()
        => $"[{Type} #{StreamIndex}] {Codec} {PixelFormatStr} {Width}x{Height} @ {FPS:#.###} | [Color: {ColorSpace}] [BR: {BitRate}] | {Utils.TicksToTime((long)(AVStream->start_time * Timebase))}/{Utils.TicksToTime((long)(AVStream->duration * Timebase))} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}";

    public VideoStream() { }
    public VideoStream(Demuxer demuxer, AVStream* st) : base(demuxer, st)
    {
        Demuxer = demuxer;
        AVStream = st;
        Refresh();
    }

    public void Refresh(AVPixelFormat format = AVPixelFormat.None, AVFrame* frame = null)
    {
        base.Refresh();

        PixelFormat     = format == AVPixelFormat.None ? (AVPixelFormat)AVStream->codecpar->format : format;
        PixelFormatStr  = PixelFormat.ToString().Replace("AV_PIX_FMT_","").ToLower();
        Width           = (uint)AVStream->codecpar->width;
        Height          = (uint)AVStream->codecpar->height;

        // TBR: Maybe required also for input formats with AVFMT_NOTIMESTAMPS (and audio/subs)
        // Possible FFmpeg.Autogen bug with Demuxer.FormatContext->iformat->flags (should be uint?) does not contain AVFMT_NOTIMESTAMPS (256 instead of 384)
        if (Demuxer.Name == "h264" || Demuxer.Name == "hevc")
        {
            FixTimestamps = true;

            if (Demuxer.Config.ForceFPS > 0)
                FPS = Demuxer.Config.ForceFPS;
            else
                FPS = av_q2d(av_guess_frame_rate(Demuxer.FormatContext, AVStream, frame));

            if (FPS == 0)
                FPS = 25;
        }
        else
        {
            FixTimestamps = false;
            FPS  = av_q2d(av_guess_frame_rate(Demuxer.FormatContext, AVStream, frame));
        }

        FrameDuration   = FPS > 0 ? (long) (10000000 / FPS) : 0;
        TotalFrames     = AVStream->duration > 0 && FrameDuration > 0 ? (int) (AVStream->duration * Timebase / FrameDuration) : (FrameDuration > 0 ? (int) (Demuxer.Duration / FrameDuration) : 0);

        int x, y;
        AVRational sar = av_guess_sample_aspect_ratio(null, AVStream, null);
        if (av_cmp_q(sar, av_make_q(0, 1)) <= 0)
            sar = av_make_q(1, 1);

        av_reduce(&x, &y, Width  * sar.Num, Height * sar.Den, 1024 * 1024);
        AspectRatio = new AspectRatio(x, y);

        if (PixelFormat != AVPixelFormat.None)
        {
            ColorRange = AVStream->codecpar->color_range == AVColorRange.Jpeg ? ColorRange.Full : ColorRange.Limited;

            if (AVStream->codecpar->color_space == AVColorSpace.Bt470bg)
                ColorSpace = ColorSpace.BT601;
            else if (AVStream->codecpar->color_space == AVColorSpace.Bt709)
                ColorSpace = ColorSpace.BT709;
            else ColorSpace = AVStream->codecpar->color_space == AVColorSpace.Bt2020Cl || AVStream->codecpar->color_space == AVColorSpace.Bt2020Ncl
                ? ColorSpace.BT2020
                : Height > 576 ? ColorSpace.BT709 : ColorSpace.BT601;

            // This causes issues
            //if (AVStream->codecpar->color_space == AVColorSpace.AVCOL_SPC_UNSPECIFIED && AVStream->codecpar->color_trc == AVColorTransferCharacteristic.AVCOL_TRC_UNSPECIFIED && Height > 1080)
            //{   // TBR: Handle Dolphy Vision?
            //    ColorSpace = ColorSpace.BT2020;
            //    ColorTransfer = AVColorTransferCharacteristic.AVCOL_TRC_SMPTE2084;
            //}
            //else
            ColorTransfer = AVStream->codecpar->color_trc;

            // We get rotation from frame side data only from 1st frame in case of exif orientation (mainly for jpeg) - TBR if required to check for each frame
            AVFrameSideData* frameSideData;
            AVPacketSideData* pktSideData;
            double rotation = 0;
            if (frame != null && (frameSideData = av_frame_get_side_data(frame, AVFrameSideDataType.Displaymatrix)) != null && frameSideData->data != null)
                rotation = -Math.Round(av_display_rotation_get((int*)frameSideData->data)); //int_array9 displayMatrix = Marshal.PtrToStructure<int_array9>((nint)frameSideData->data); TBR: NaN why?
            else if ((pktSideData = av_packet_side_data_get(AVStream->codecpar->coded_side_data, AVStream->codecpar->nb_coded_side_data, AVPacketSideDataType.Displaymatrix)) != null && pktSideData->data != null)
                rotation = -Math.Round(av_display_rotation_get((int*)pktSideData->data));

            Rotation = rotation - (360*Math.Floor(rotation/360 + 0.9/360));

            PixelFormatDesc = av_pix_fmt_desc_get(PixelFormat);
            var comps       = PixelFormatDesc->comp.ToArray();
            PixelComps      = new AVComponentDescriptor[PixelFormatDesc->nb_components];
            for (int i=0; i<PixelComps.Length; i++)
                PixelComps[i] = comps[i];

            PixelInterleaved= PixelFormatDesc->log2_chroma_w != PixelFormatDesc->log2_chroma_h;
            IsRGB           = (PixelFormatDesc->flags & PixFmtFlags.Rgb) != 0;

            PixelSameDepth  = true;
            PixelPlanes     = 0;
            if (PixelComps.Length > 0)
            {
                PixelComp0Depth = PixelComps[0].depth;
                int prevBit     = PixelComp0Depth;
                for (int i=0; i<PixelComps.Length; i++)
                {
                    if (PixelComps[i].plane > PixelPlanes)
                        PixelPlanes = PixelComps[i].plane;

                    if (prevBit != PixelComps[i].depth)
                        PixelSameDepth = false;
                }

                PixelPlanes++;
            }
        }
    }
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.ViewModels;

public class SelectLanguageDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }

    public SelectLanguageDialogVM(FlyleafManager fl)
    {
        FL = fl;

        _filteredAvailableLanguages = CollectionViewSource.GetDefaultView(AvailableLanguages);
        _filteredAvailableLanguages.Filter = obj =>
        {
            if (obj is not Language lang)
                return false;

            if (string.IsNullOrWhiteSpace(SearchText))
                return true;

            string query = SearchText.Trim();

            if (lang.TopEnglishName.Contains(query, StringComparison.OrdinalIgnoreCase))
                return true;

            return lang.TopNativeName.Contains(query, StringComparison.OrdinalIgnoreCase);
        };
    }

    public string SearchText
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                _filteredAvailableLanguages.Refresh();
            }
        }
    } = string.Empty;

    private readonly ICollectionView _filteredAvailableLanguages;
    public ObservableCollection<Language> AvailableLanguages { get; } = new();
    public ObservableCollection<Language> SelectedLanguages { get; } = new();

    public Language? SelectedAvailable
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanMoveRight));
            }
        }
    }

    // TODO: L: weird naming
    public Language? SelectedSelected
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanMoveLeft));
                OnPropertyChanged(nameof(CanMoveUp));
                OnPropertyChanged(nameof(CanMoveDown));
            }
        }
    }

    public bool CanMoveRight => SelectedAvailable != null;
    public bool CanMoveLeft => SelectedSelected != null;
    public bool CanMoveUp => SelectedSelected != null && SelectedLanguages.IndexOf(SelectedSelected) > 0;
    public bool CanMoveDown => SelectedSelected != null && SelectedLanguages.IndexOf(SelectedSelected) < SelectedLanguages.Count - 1;

    public DelegateCommand? CmdMoveRight => field ??= new DelegateCommand(() =>
    {
        if (SelectedAvailable != null && !SelectedLanguages.Contains(SelectedAvailable))
        {
            SelectedLanguages.Add(SelectedAvailable);
            AvailableLanguages.Remove(SelectedAvailable);
        }
    }).ObservesCanExecute(() => CanMoveRight);

    public DelegateCommand? CmdMoveLeft => field ??= new DelegateCommand(() =>
    {
        if (SelectedSelected != null)
        {
            // conform to order
            int insertIndex = 0;
            while (insertIndex < AvailableLanguages.Count &&
                   string.Compare(AvailableLanguages[insertIndex].TopEnglishName, SelectedSelected.TopEnglishName, StringComparison.InvariantCulture) < 0)
            {
                insertIndex++;
            }

            AvailableLanguages.Insert(insertIndex, SelectedSelected);

            SelectedLanguages.Remove(SelectedSelected);
        }
    }).ObservesCanExecute(() => CanMoveLeft);

    public DelegateCommand? CmdMoveUp => field ??= new DelegateCommand(() =>
    {
        int index = SelectedLanguages.IndexOf(SelectedSelected!);
        if (index > 0)
        {
            SelectedLanguages.Move(index, index - 1);
            OnPropertyChanged(nameof(CanMoveUp));
            OnPropertyChanged(nameof(CanMoveDown));
        }
    }).ObservesCanExecute(() => CanMoveUp);

    public DelegateCommand? CmdMoveDown => field ??= new DelegateCommand(() =>
    {
        int index = SelectedLanguages.IndexOf(SelectedSelected!);
        if (index < SelectedLanguages.Count - 1)
        {
            SelectedLanguages.Move(index, index + 1);
            OnPropertyChanged(nameof(CanMoveUp));
            OnPropertyChanged(nameof(CanMoveDown));
        }
    }).ObservesCanExecute(() => CanMoveDown);

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); }
        = $"Select Language - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 600;
    public double WindowHeight { get; set => Set(ref field, value); } = 360;

    public DialogCloseListener RequestClose { get; }

    public bool CanCloseDialog()
    {
        return true;
    }
    public void OnDialogClosed()
    {
        List<Language> langs = [.. SelectedLanguages];
        DialogParameters p = new()
        {
            { "languages", langs }
        };

        RequestClose.Invoke(p);
    }

    public void OnDialogOpened(IDialogParameters parameters)
    {
        List<Language> langs = parameters.GetValue<List<Language>>("languages");

        foreach (var lang in langs)
        {
            SelectedLanguages.Add(lang);
        }

        foreach (Language lang in Language.AllLanguages)
        {
            if (!SelectedLanguages.Contains(lang))
            {
                AvailableLanguages.Add(lang);
            }
        }
    }
    #endregion
}

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;

using FlyleafLib.Controls;
using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaDemuxer;

using static FlyleafLib.Utils;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

public unsafe partial class Player : NotifyPropertyChanged, IDisposable
{
    #region Properties
    public bool                 IsDisposed          { get; private set; }

    /// <summary>
    /// FlyleafHost (WinForms, WPF or WinUI)
    /// </summary>
    public IHostPlayer          Host                { get => _Host; set => Set(ref _Host, value); }
    IHostPlayer _Host;

    /// <summary>
    /// Player's Activity (Idle/Active/FullActive)
    /// </summary>
    public Activity             Activity            { get; private set; }

    /// <summary>
    /// Helper ICommands for WPF MVVM
    /// </summary>
    public Commands             Commands            { get; private set; }

    public Playlist             Playlist            => decoder.Playlist;

    /// <summary>
    /// Player's Audio (In/Out)
    /// </summary>
    public Audio                Audio               { get; private set; }

    /// <summary>
    /// Player's Video
    /// </summary>
    public Video                Video               { get; private set; }

    /// <summary>
    /// Player's Subtitles
    /// </summary>
    public Subtitles            Subtitles           { get; private set; }

    /// <summary>
    /// Player's Data
    /// </summary>
    public Data                 Data                { get; private set; }

    /// <summary>
    /// Player's Renderer
    /// (Normally you should not access this directly)
    /// </summary>
    public Renderer             renderer            => decoder.VideoDecoder.Renderer;

    /// <summary>
    /// Player's Decoder Context
    /// (Normally you should not access this directly)
    /// </summary>
    public DecoderContext       decoder             { get; private set; }

    /// <summary>
    /// Audio Decoder
    /// (Normally you should not access this directly)
    /// </summary>
    public AudioDecoder         AudioDecoder        => decoder.AudioDecoder;

    /// <summary>
    /// Video Decoder
    /// (Normally you should not access this directly)
    /// </summary>
    public VideoDecoder         VideoDecoder        => decoder.VideoDecoder;

    /// <summary>
    /// Subtitles Decoder
    /// (Normally you should not access this directly)
    /// </summary>
    public SubtitlesDecoder[]   SubtitlesDecoders   => decoder.SubtitlesDecoders;

    /// <summary>
    /// Data Decoder
    /// (Normally you should not access this directly)
    /// </summary>
    public DataDecoder          DataDecoder         => decoder.DataDecoder;

    /// <summary>
    /// Main Demuxer (if video disabled or audio only can be AudioDemuxer instead of VideoDemuxer)
    /// (Normally you should not access this directly)
    /// </summary>
    public Demuxer              MainDemuxer         => decoder.MainDemuxer;

    /// <summary>
    /// Audio Demuxer
    /// (Normally you should not access this directly)
    /// </summary>
    public Demuxer              AudioDemuxer        => decoder.AudioDemuxer;

    /// <summary>
    /// Video Demuxer
    /// (Normally you should not access this directly)
    /// </summary>
    public Demuxer              VideoDemuxer        => decoder.VideoDemuxer;

    /// <summary>
    /// Subtitles Demuxer
    /// (Normally you should not access this directly)
    /// </summary>
    public Demuxer[]            SubtitlesDemuxers  => decoder.SubtitlesDemuxers;

    /// <summary>
    /// Subtitles Manager
    /// </summary>
    public SubtitlesManager     SubtitlesManager   => decoder.SubtitlesManager;

    /// <summary>
    /// Subtitles OCR
    /// </summary>
    public SubtitlesOCR         SubtitlesOCR       => decoder.SubtitlesOCR;

    /// <summary>
    /// Subtitles ASR
    /// </summary>
    public SubtitlesASR         SubtitlesASR       => decoder.SubtitlesASR;

    /// <summary>
    /// Data Demuxer
    /// (Normally you should not access this directly)
    /// </summary>
    public Demuxer DataDemuxer => decoder.DataDemuxer;

    /// <summary>
    /// Player's incremental unique id
    /// </summary>
    public int          PlayerId            { get; private set; }

    /// <summary>
    /// Player's configuration (set once in the constructor)
    /// </summary>
    public Config       Config              { get; protected set; }

    /// <summary>
    /// Player's Status
    /// </summary>
    public Status       Status
    {
        get => status;
        private set
        {
            var prev = _Status;

            if (Set(ref _Status, value))
            {
                // Loop Playback
                if (value == Status.Ended)
                {
                    if (LoopPlayback && !ReversePlayback)
                    {
                        int seekMs = (int)(MainDemuxer.StartTime == 0 ? 0 : MainDemuxer.StartTime / 10000);
                        Seek(seekMs);
                    }
                }

                if (prev == Status.Opening || value == Status.Opening)
                {
                    Raise(nameof(IsOpening));
                }
            }
        }
    }

    Status _Status = Status.Stopped, status = Status.Stopped;
    public bool         IsPlaying           => status == Status.Playing;
    public bool         IsOpening           => status == Status.Opening;

    /// <summary>
    /// Whether the player's status is capable of accepting playback commands
    /// </summary>
    public bool         CanPlay             { get => canPlay;           internal set => Set(ref _CanPlay, value); }
    internal bool _CanPlay, canPlay;

    /// <summary>
    /// The list of chapters
    /// </summary>
    public ObservableCollection<Demuxer.Chapter>
                        Chapters            => VideoDemuxer?.Chapters;

    /// <summary>
    /// Player's current time or user's current seek time (uses backward direction or accurate seek based on Config.Player.SeekAccurate)
    /// </summary>
    public long         CurTime             { get => curTime;           set { if (Config.Player.SeekAccurate) SeekAccurate((int) (value/10000)); else Seek((int) (value/10000), false); } } // Note: forward seeking casues issues to some formats and can have serious delays (eg. dash with h264, dash with vp9 works fine)
    long _CurTime, curTime;
    internal void UpdateCurTime()
    {
        lock (seeks)
        {
            if (MainDemuxer == null || !seeks.IsEmpty)
                return;

            if (MainDemuxer.IsHLSLive)
            {
                curTime  = MainDemuxer.CurTime; // *speed ?
                duration = MainDemuxer.Duration;
                Duration = Duration;
            }
        }

        if (Set(ref _CurTime, curTime, true, nameof(CurTime)))
        {
            Raise(nameof(RemainingDuration));
        }

        UpdateBufferedDuration();
    }
    internal void UpdateBufferedDuration()
    {
        if (_BufferedDuration != MainDemuxer.BufferedDuration)
        {
            _BufferedDuration = MainDemuxer.BufferedDuration;
            Raise(nameof(BufferedDuration));
        }
    }

    public long         RemainingDuration => Duration - CurTime;

    /// <summary>
    /// Input's duration
    /// </summary>
    public long         Duration
    {
        get => duration;
        private set
        {
            if (Set(ref _Duration, value))
            {
                Raise(nameof(RemainingDuration));
            }
        }
    }
    long _Duration, duration;

    /// <summary>
    /// Forces Player's and Demuxer's Duration to allow Seek
    /// </summary>
    /// <param name="duration">Duration (Ticks)</param>
    /// <exception cref="ArgumentNullException">Demuxer must be opened before forcing the duration</exception>
    public void ForceDuration(long duration)
    {
        if (MainDemuxer == null)
            throw new ArgumentNullException(nameof(MainDemuxer));

        this.duration = duration;
        MainDemuxer.ForceDuration(duration);
        isLive = MainDemuxer.IsLive;
        UI(() =>
        {
            Duration= Duration;
            IsLive  = IsLive;
        });
    }

    /// <summary>
    /// The current buffered duration in the demuxer
    /// </summary>
    public long         BufferedDuration    { get => MainDemuxer == null ? 0 : MainDemuxer.BufferedDuration;
                                                                        internal set => Set(ref _BufferedDuration, value); }
    long _BufferedDuration;

    /// <summary>
    /// Whether the input is live (duration might not be 0 on live sessions to allow live seek, eg. hls)
    /// </summary>
    public bool         IsLive              { get => isLive;            private set => Set(ref _IsLive, value); }
    bool _IsLive, isLive;

    ///// <summary>
    ///// Total bitrate (Kbps)
    ///// </summary>
    public double       BitRate             { get => bitRate;           internal set => Set(ref _BitRate, value); }
    internal double _BitRate, bitRate;

    /// <summary>
    /// Whether the player is recording
    /// </summary>
    public bool         IsRecording
    {
        get => decoder != null && decoder.IsRecording;
        private set { if (_IsRecording == value) return; _IsRecording = value; UI(() => Set(ref _IsRecording, value, false)); }
    }
    bool _IsRecording;

    /// <summary>
    /// Pan X Offset to change the X location
    /// </summary>
    public int          PanXOffset          { get => renderer.PanXOffset; set { renderer.PanXOffset = value; Raise(nameof(PanXOffset)); } }

    /// <summary>
    /// Pan Y Offset to change the Y location
    /// </summary>
    public int          PanYOffset          { get => renderer.PanYOffset; set { renderer.PanYOffset = value; Raise(nameof(PanYOffset)); } }

    /// <summary>
    /// Playback's speed (x1 - x4)
    /// </summary>
    public double       Speed {
        get => speed;
        set
        {
            double newValue = Math.Round(value, 3);
            if (value < 0.125)
                newValue = 0.125;
            else if (value > 16)
                newValue = 16;

            if (newValue == speed)
                return;

            AudioDecoder.Speed      = newValue;
            VideoDecoder.Speed      = newValue;
            speed                   = newValue;
            decoder.RequiresResync  = true;
            requiresBuffering       = true;
            for (int i = 0; i < subNum; i++)
            {
                sFramesPrev[i] = null;
            }
            SubtitleClear();
            UI(() =>
            {
                Raise(nameof(Speed));
            });
        }
    }
    double speed = 1;

    /// <summary>
    /// Pan zoom percentage (100 for 100%)
    /// </summary>
    public int          Zoom
    {
        get => (int)(renderer.Zoom * 100);
        set { renderer.SetZoom(renderer.Zoom = value / 100.0); RaiseUI(nameof(Zoom)); }
        //set { renderer.SetZoomAndCenter(renderer.Zoom = value / 100.0, Renderer.ZoomCenterPoint); RaiseUI(nameof(Zoom)); } // should reset the zoom center point?
    }

    /// <summary>
    /// Pan rotation angle (for D3D11 VP allowed values are 0, 90, 180, 270 only)
    /// </summary>
    public uint Rotation            { get => renderer.Rotation;
        set
        {
            renderer.Rotation = value;
            RaiseUI(nameof(Rotation));
        }
    }

    /// <summary>
    /// Pan Horizontal Flip (FlyleafVP only)
    /// </summary>
    public bool HFlip { get => renderer.HFlip; set => renderer.HFlip = value; }

    /// <summary>
    /// Pan Vertical Flip (FlyleafVP only)
    /// </summary>
    public bool VFlip { get => renderer.VFlip; set => renderer.VFlip = value; }

    /// <summary>
    /// Whether to use reverse playback mode
    /// </summary>
    public bool         ReversePlayback
    {
        get => _ReversePlayback;

        set
        {
            if (_ReversePlayback == value)
                return;

            _ReversePlayback = value;
            UI(() => Set(ref _ReversePlayback, value, false));

            if (!Video.IsOpened || !CanPlay | IsLive)
                return;

            lock (lockActions)
            {
                bool shouldPlay = IsPlaying || (Status == Status.Ended && Config.Player.AutoPlay);
                Pause();
                dFrame = null;

                for (int i = 0; i < subNum; i++)
                {
                    sFrames[i] = null;
                    SubtitleClear(i);
                }

                decoder.StopThreads();
                decoder.Flush();

                if (Status == Status.Ended)
                {
                    status = Status.Paused;
                    UI(() => Status = Status);
                }

                if (value)
                {
                    Speed = 1;
                    VideoDemuxer.EnableReversePlayback(CurTime);
                }
                else
                {
                    VideoDemuxer.DisableReversePlayback();

                    var vFrame = VideoDecoder.GetFrame(VideoDecoder.GetFrameNumber(CurTime));
                    VideoDecoder.DisposeFrame(vFrame);
                    vFrame = null;
                    decoder.RequiresResync = true;
                }

                reversePlaybackResync = false;
                if (shouldPlay) Play();
            }
        }
    }
    bool _ReversePlayback;

    public bool         LoopPlayback        { get; set => Set(ref field, value); }

    public object       Tag                 { get => tag; set => Set(ref  tag, value); }
    object tag;

    public string       LastError           { get => lastError; set => Set(ref _LastError, value); }
    string _LastError, lastError;

    public string       OSDMessage          { get; set => Set(ref field, value, false); }

    public event        EventHandler<KnownErrorOccurredEventArgs> KnownErrorOccurred;
    public event        EventHandler<UnknownErrorOccurredEventArgs> UnknownErrorOccurred;

    bool decoderHasEnded => decoder != null && (VideoDecoder.Status == MediaFramework.Status.Ended || (VideoDecoder.Disposed && AudioDecoder.Status == MediaFramework.Status.Ended));
    #endregion

    #region Properties Internal
    readonly object lockActions  = new();
    readonly object lockSubtitles= new();

    bool taskSeekRuns;
    bool taskPlayRuns;
    bool taskOpenAsyncRuns;

    readonly ConcurrentStack<SeekData>   seeks      = new();
    readonly ConcurrentQueue<Action>     UIActions  = new();

    internal AudioFrame     aFrame;
    internal VideoFrame     vFrame;
    internal SubtitlesFrame[] sFrames, sFramesPrev;
    internal DataFrame      dFrame;
    internal PlayerStats    stats = new();
    internal LogHandler     Log;

    internal bool requiresBuffering;
    bool reversePlaybackResync;

    bool isVideoSwitch;
    bool isAudioSwitch;
    bool[] isSubsSwitches;
    bool isDataSwitch;
    #endregion

    public Player(Config config = null)
    {
        if (config != null)
        {
            if (config.Player.player != null)
                throw new Exception("Player's configuration is already assigned to another player");

            Config = config;
        }
        else
            Config = new Config();

        PlayerId = GetUniqueId();
        Log = new LogHandler(("[#" + PlayerId + "]").PadRight(8, ' ') + " [Player        ] ");
        Log.Debug($"Creating Player (Usage = {Config.Player.Usage})");

        Activity    = new Activity(this);
        Audio       = new Audio(this);
        Video       = new Video(this);
        Subtitles   = new Subtitles(this);
        Data        = new Data(this);
        Commands    = new Commands(this);

        Config.SetPlayer(this);

        if (Config.Player.Usage == Usage.Audio)
        {
            Config.Video.Enabled = false;
            Config.Subtitles.Enabled = false;
        }

        decoder = new DecoderContext(Config, PlayerId) { Tag = this };
        Engine.AddPlayer(this);

        if (decoder.VideoDecoder.Renderer != null)
            decoder.VideoDecoder.Renderer.forceNotExtractor = true;

        //decoder.OpenPlaylistItemCompleted              += Decoder_OnOpenExternalSubtitlesStreamCompleted;

        decoder.OpenAudioStreamCompleted               += Decoder_OpenAudioStreamCompleted;
        decoder.OpenVideoStreamCompleted               += Decoder_OpenVideoStreamCompleted;
        decoder.OpenSubtitlesStreamCompleted           += Decoder_OpenSubtitlesStreamCompleted;
        decoder.OpenDataStreamCompleted                += Decoder_OpenDataStreamCompleted;

        decoder.OpenExternalAudioStreamCompleted       += Decoder_OpenExternalAudioStreamCompleted;
        decoder.OpenExternalVideoStreamCompleted       += Decoder_OpenExternalVideoStreamCompleted;
        decoder.OpenExternalSubtitlesStreamCompleted   += Decoder_OpenExternalSubtitlesStreamCompleted;

        AudioDecoder.CBufAlloc      = () => { if (aFrame != null) aFrame.dataPtr = IntPtr.Zero; aFrame = null; Audio.ClearBuffer(); aFrame = null; };
        AudioDecoder.CodecChanged   = Decoder_AudioCodecChanged;
        VideoDecoder.CodecChanged   = Decoder_VideoCodecChanged;
        decoder.RecordingCompleted += (o, e) => { IsRecording = false; };
        Chapters.CollectionChanged += (o, e) => { RaiseUI(nameof(Chapters)); };

        // second subtitles
        sFrames = new SubtitlesFrame[subNum];
        sFramesPrev = new SubtitlesFrame[subNum];
        sDistanceMss = new int[subNum];
        isSubsSwitches = new bool[subNum];

        status = Status.Stopped;
        Reset();
        Log.Debug("Created");
    }

    /// <summary>
    /// Disposes the Player and de-assigns it from FlyleafHost
    /// </summary>
    public void Dispose() => Engine.DisposePlayer(this);
    internal void DisposeInternal()
    {
        lock (lockActions)
        {
            if (IsDisposed)
                return;

            try
            {
                Initialize();
                Audio.Dispose();
                decoder.Dispose();
                Host?.Player_Disposed();
                Log.Info("Disposed");
            } catch (Exception e) { Log.Warn($"Disposed ({e.Message})"); }

            IsDisposed = true;
        }
    }
    internal void RefreshMaxVideoFrames()
    {
        lock (lockActions)
        {
            if (!Video.isOpened)
                return;

            bool wasPlaying = IsPlaying;
            Pause();
            VideoDecoder.RefreshMaxVideoFrames();
            ReSync(decoder.VideoStream, (int) (CurTime / 10000), true);

            if (wasPlaying)
                Play();
        }
    }

    private void ResetMe()
    {
        canPlay     = false;
        bitRate     = 0;
        curTime     = 0;
        duration    = 0;
        isLive      = false;
        lastError   = null;

        UIAdd(() =>
        {
            BitRate     = BitRate;
            Duration    = Duration;
            IsLive      = IsLive;
            Status      = Status;
            CanPlay     = CanPlay;
            LastError   = LastError;
            BufferedDuration = 0;
            Set(ref _CurTime, curTime, true, nameof(CurTime));
        });
    }
    private void Reset()
    {
        ResetMe();
        Video.Reset();
        Audio.Reset();
        Subtitles.Reset();
        UIAll();
    }
    private void Initialize(Status status = Status.Stopped, bool andDecoder = true, bool isSwitch = false)
    {
        if (CanDebug) Log.Debug($"Initializing");

        lock (lockActions) // Required in case of OpenAsync and Stop requests
        {
            try
            {
                Engine.TimeBeginPeriod1();

                this.status = status;
                canPlay = false;
                isVideoSwitch = false;
                seeks.Clear();

                while (taskPlayRuns || taskSeekRuns) Thread.Sleep(5);

                if (andDecoder)
                {
                    if (isSwitch)
                        decoder.InitializeSwitch();
                    else
                        decoder.Initialize();
                }

                Reset();
                VideoDemuxer.DisableReversePlayback();
                ReversePlayback = false;

                if (CanDebug) Log.Debug($"Initialized");

            } catch (Exception e)
            {
                Log.Error($"Initialize() Error: {e.Message}");

            } finally
            {
                Engine.TimeEndPeriod1();
            }
        }
    }

    internal void UIAdd(Action action) => UIActions.Enqueue(action);
    internal void UIAll()
    {
        while (!UIActions.IsEmpty)
            if (UIActions.TryDequeue(out var action))
                UI(action);
    }

    public override bool Equals(object obj)
        => obj == null || !(obj is Player) ? false : ((Player)obj).PlayerId == PlayerId;
    public override int GetHashCode() => PlayerId.GetHashCode();

    // Avoid having this code in OnPaintBackground as it can cause designer issues (renderer will try to load FFmpeg.Autogen assembly because of HDR Data)
    internal bool WFPresent() { if (renderer == null || renderer.SCDisposed) return false; renderer?.Present(); return true; }

    internal void RaiseKnownErrorOccurred(string message, KnownErrorType errorType)
    {
        KnownErrorOccurred?.Invoke(this, new KnownErrorOccurredEventArgs
        {
            Message = message,
            ErrorType = errorType
        });
    }

    internal void RaiseUnknownErrorOccurred(string message, UnknownErrorType errorType, Exception exception = null)
    {
        UnknownErrorOccurred?.Invoke(this, new UnknownErrorOccurredEventArgs
        {
            Message = message,
            ErrorType = errorType,
            Exception = exception
        });
    }
}

public enum KnownErrorType
{
    Configuration,
    ASR
}

public class KnownErrorOccurredEventArgs : EventArgs
{
    public required string Message { get; init; }
    public required KnownErrorType ErrorType { get; init; }
}

public enum UnknownErrorType
{
    Translation,
    Subtitles,
    ASR,
    Playback,
    Network
}

public class UnknownErrorOccurredEventArgs : EventArgs
{
    public required string Message { get; init; }
    public required UnknownErrorType ErrorType { get; init; }
    public Exception Exception { get; init; }
}

public enum Status
{
    Opening,
    Failed,
    Stopped,
    Paused,
    Playing,
    Ended
}
public enum Usage
{
    AVS,
    Audio
}

using System.Windows;
using System.Windows.Controls;

namespace LLPlayer.Controls;

// The standard WrapPanel does not have a centering function, so add it.
// ref: https://stackoverflow.com/a/7747002
public class AlignableWrapPanel : Panel
{
    public static readonly DependencyProperty HorizontalContentAlignmentProperty =
        DependencyProperty.Register(nameof(HorizontalContentAlignment), typeof(HorizontalAlignment), typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange));
    public HorizontalAlignment HorizontalContentAlignment
    {
        get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
        set => SetValue(HorizontalContentAlignmentProperty, value);
    }

    protected override Size MeasureOverride(Size constraint)
    {
        Size curLineSize = new();
        Size panelSize = new();

        UIElementCollection children = InternalChildren;

        for (int i = 0; i < children.Count; i++)
        {
            UIElement child = children[i];

            // Flow passes its own constraint to children
            child.Measure(constraint);
            Size sz = child.DesiredSize;

            if (child is NewLine ||
                curLineSize.Width + sz.Width > constraint.Width) //need to switch to another line
            {
                panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
                panelSize.Height += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line
                {
                    panelSize.Width = Math.Max(sz.Width, panelSize.Width);
                    panelSize.Height += sz.Height;
                    curLineSize = new Size();
                }
            }
            else //continue to accumulate a line
            {
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            }
        }

        // the last line size, if any need to be added
        panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
        panelSize.Height += curLineSize.Height;

        return panelSize;
    }

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        int firstInLine = 0;
        Size curLineSize = new();
        double accumulatedHeight = 0;
        UIElementCollection children = InternalChildren;

        for (int i = 0; i < children.Count; i++)
        {
            UIElement child = children[i];

            Size sz = child.DesiredSize;

            if (child is NewLine ||
                curLineSize.Width + sz.Width > arrangeBounds.Width) //need to switch to another line
            {
                ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);

                accumulatedHeight += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line
                {
                    ArrangeLine(accumulatedHeight, sz, arrangeBounds.Width, i, ++i);
                    accumulatedHeight += sz.Height;
                    curLineSize = new Size();
                }
                firstInLine = i;
            }
            else //continue to accumulate a line
            {
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            }
        }

        if (firstInLine < children.Count)
            ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);

        return arrangeBounds;
    }

    private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
    {
        double x = 0;
        if (this.HorizontalContentAlignment == HorizontalAlignment.Center)
        {
            x = (boundsWidth - lineSize.Width) / 2;
        }
        else if (this.HorizontalContentAlignment == HorizontalAlignment.Right)
        {
            x = (boundsWidth - lineSize.Width);
        }

        UIElementCollection children = InternalChildren;
        for (int i = start; i < end; i++)
        {
            UIElement child = children[i];
            child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineSize.Height));
            x += child.DesiredSize.Width;
        }
    }
}

using System.Diagnostics;
using System.Threading;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaStream;
using static FlyleafLib.Utils;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

unsafe partial class Player
{
    /// <summary>
    /// Fires on Data frame when it's supposed to be shown according to the stream
    /// Warning: Uses Invoke and it comes from playback thread so you can't pause/stop etc. You need to use another thread if you have to.
    /// </summary>
    public event EventHandler<DataFrame> OnDataFrame;

    /// <summary>
    /// Fires on buffering started
    /// Warning: Uses Invoke and it comes from playback thread so you can't pause/stop etc. You need to use another thread if you have to.
    /// </summary>
    public event EventHandler BufferingStarted;
    protected virtual void OnBufferingStarted()
    {
        if (onBufferingStarted != onBufferingCompleted) return;
        BufferingStarted?.Invoke(this, new EventArgs());
        onBufferingStarted++;

        if (CanDebug) Log.Debug($"OnBufferingStarted");
    }

    /// <summary>
    /// Fires on buffering completed (will fire also on failed buffering completed)
    /// (BufferDration > Config.Player.MinBufferDuration)
    /// Warning: Uses Invoke and it comes from playback thread so you can't pause/stop etc. You need to use another thread if you have to.
    /// </summary>
    public event EventHandler<BufferingCompletedArgs> BufferingCompleted;
    protected virtual void OnBufferingCompleted(string error = null)
    {
        if (onBufferingStarted - 1 != onBufferingCompleted) return;

        if (error != null && LastError == null)
        {
            lastError = error;
            UI(() => LastError = LastError);
        }

        BufferingCompleted?.Invoke(this, new BufferingCompletedArgs(error));
        onBufferingCompleted++;
        if (CanDebug) Log.Debug($"OnBufferingCompleted{(error != null ? $" (Error: {error})" : "")}");
    }

    long    onBufferingStarted;
    long    onBufferingCompleted;

    int     vDistanceMs;
    int     aDistanceMs;
    int[]   sDistanceMss;
    int     dDistanceMs;
    int     sleepMs;

    long    elapsedTicks;
    long    elapsedSec;
    long    startTicks;
    long    showOneFrameTicks;

    int     allowedLateAudioDrops;
    long    lastSpeedChangeTicks;
    long    curLatency;
    internal long curAudioDeviceDelay;

    public int subNum => Config.Subtitles.Max;

    Stopwatch sw = new();

    private void ShowOneFrame()
    {
        for (int i = 0; i < subNum; i++)
        {
            sFrames[i] = null;
        }
        if (VideoDecoder.Frames.IsEmpty || !VideoDecoder.Frames.TryDequeue(out vFrame))
            return;

        renderer.Present(vFrame);

        if (!seeks.IsEmpty)
            return;

        if (!VideoDemuxer.IsHLSLive)
            curTime = vFrame.timestamp;

        UI(() => UpdateCurTime());

        for (int i = 0; i < subNum; i++)
        {
            // Prevents blinking after seek
            if (sFramesPrev[i] != null)
            {
                var cur = SubtitlesManager[i].GetCurrent();
                if (cur != null)
                {
                    if (!string.IsNullOrEmpty(cur.DisplayText) || (cur.IsBitmap && cur.Bitmap != null))
                    {
                        continue;
                    }
                }
            }

            // Clear last subtitles text if video timestamp is not within subs timestamp + duration (to prevent clearing current subs on pause/play)
            if (sFramesPrev[i] == null || sFramesPrev[i].timestamp > vFrame.timestamp || (sFramesPrev[i].timestamp + (sFramesPrev[i].duration * (long)10000)) < vFrame.timestamp)
            {
                sFramesPrev[i] = null;
                SubtitleClear(i);
            }
        }

        // Required for buffering on paused
        if (decoder.RequiresResync && !IsPlaying && seeks.IsEmpty)
            decoder.Resync(vFrame.timestamp);

        vFrame = null;
    }

    // !!! NEEDS RECODING (We show one frame, we dispose it, we get another one and we show it also after buffering which can be in 'no time' which can leave us without any more decoded frames so we rebuffer)
    private bool MediaBuffer()
    {
        if (CanTrace) Log.Trace("Buffering");

        while (isVideoSwitch && IsPlaying) Thread.Sleep(10);

        Audio.ClearBuffer();

        VideoDemuxer.Start();
        VideoDecoder.Start();

        if (Audio.isOpened && Config.Audio.Enabled)
        {
            curAudioDeviceDelay = Audio.GetDeviceDelay();

            if (AudioDecoder.OnVideoDemuxer)
                AudioDecoder.Start();
            else if (!decoder.RequiresResync)
            {
                AudioDemuxer.Start();
                AudioDecoder.Start();
            }
        }

        if (Config.Subtitles.Enabled)
        {
            for (int i = 0; i < subNum; i++)
            {
                if (!Subtitles[i].IsOpened)
                {
                    continue;
                }

                lock (lockSubtitles)
                {
                    if (SubtitlesDecoders[i].OnVideoDemuxer)
                    {
                        SubtitlesDecoders[i].Start();
                    }
                    //else if (!decoder.RequiresResync)
                    //{
                    //    SubtitlesDemuxers[i].Start();
                    //    SubtitlesDecoders[i].Start();
                    //}
                }
            }
        }

        if (Data.isOpened && Config.Data.Enabled)
        {
            if (DataDecoder.OnVideoDemuxer)
                DataDecoder.Start();
            else if (!decoder.RequiresResync)
            {
                DataDemuxer.Start();
                DataDecoder.Start();
            }
        }

        VideoDecoder.DisposeFrame(vFrame);
        vFrame = null;
        aFrame = null;
        for (int i = 0; i < subNum; i++)
        {
            sFrames[i] = null;
        }
        dFrame = null;

        bool gotAudio       = !Audio.IsOpened || Config.Player.MaxLatency != 0;
        bool gotVideo       = false;
        bool shouldStop     = false;
        bool showOneFrame   = true;
        int  audioRetries   = 4;
        int  loops          = 0;

        if (Config.Player.MaxLatency != 0)
        {
            lastSpeedChangeTicks = DateTime.UtcNow.Ticks;
            showOneFrame = false;
            Speed = 1;
        }

        do
        {
            loops++;

            if (showOneFrame && !VideoDecoder.Frames.IsEmpty)
            {
                ShowOneFrame();
                showOneFrameTicks = DateTime.UtcNow.Ticks;
                showOneFrame = false;
            }

            // We allo few ms to show a frame before cancelling
            if ((!showOneFrame || loops > 8) && !seeks.IsEmpty)
                return false;

            if (!gotVideo && !showOneFrame && !VideoDecoder.Frames.IsEmpty)
            {
                VideoDecoder.Frames.TryDequeue(out vFrame);
                if (vFrame != null) gotVideo = true;
            }

            if (!gotAudio && aFrame == null && !AudioDecoder.Frames.IsEmpty)
                AudioDecoder.Frames.TryDequeue(out aFrame);

            if (gotVideo)
            {
                if (decoder.RequiresResync)
                    decoder.Resync(vFrame.timestamp);

                if (!gotAudio && aFrame != null)
                {
                    for (int i=0; i<Math.Min(20, AudioDecoder.Frames.Count); i++)
                    {
                        if (aFrame == null
                            || aFrame.timestamp - curAudioDeviceDelay > vFrame.timestamp
                            || vFrame.timestamp > Duration)
                        {
                            gotAudio = true;
                            break;
                        }

                        if (CanTrace) Log.Trace($"Drop aFrame {TicksToTime(aFrame.timestamp)}");
                        AudioDecoder.Frames.TryDequeue(out aFrame);
                    }

                    // Avoid infinite loop in case of all audio timestamps wrong
                    if (!gotAudio)
                    {
                        audioRetries--;

                        if (audioRetries < 1)
                        {
                            gotAudio = true;
                            aFrame = null;
                            Log.Warn($"Audio Exhausted 1");
                        }
                    }
                }
            }

            if (!IsPlaying || decoderHasEnded)
                shouldStop = true;
            else
            {
                if (!VideoDecoder.IsRunning && !isVideoSwitch)
                {
                    Log.Warn("Video Exhausted");
                    shouldStop= true;
                }

                if (gotVideo && !gotAudio && audioRetries > 0 && (!AudioDecoder.IsRunning || AudioDecoder.Demuxer.Status == MediaFramework.Status.QueueFull))
                {
                    if (CanWarn) Log.Warn($"Audio Exhausted 2 | {audioRetries}");

                    audioRetries--;

                    if (audioRetries < 1)
                        gotAudio  = true;
                }
            }

            Thread.Sleep(10);

        } while (!shouldStop && (!gotVideo || !gotAudio));

        if (shouldStop && !(decoderHasEnded && IsPlaying && vFrame != null))
        {
            Log.Info("Stopped");
            return false;
        }

        if (vFrame == null)
        {
            Log.Error("No Frames!");
            return false;
        }

        // Negative Buffer Duration during codec change (we don't dipose the cached frames or we receive them later) *skip waiting for now
        var bufDuration = GetBufferedDuration();
        if (bufDuration >= 0)
            while(seeks.IsEmpty && bufDuration < Config.Player.MinBufferDuration && IsPlaying && VideoDemuxer.IsRunning && VideoDemuxer.Status != MediaFramework.Status.QueueFull)
            {
                Thread.Sleep(20);
                bufDuration = GetBufferedDuration();
                if (bufDuration < 0)
                    break;
            }

        if (!seeks.IsEmpty)
            return false;

        if (CanInfo) Log.Info($"Started [V: {TicksToTime(vFrame.timestamp)}]" + (aFrame == null ? "" : $" [A: {TicksToTime(aFrame.timestamp)}]"));

        decoder.OpenedPlugin.OnBufferingCompleted();

        return true;
    }
    private void Screamer()
    {
        long audioBufferedDuration = 0; // We force audio resync with = 0

        while (Status == Status.Playing)
        {
            if (seeks.TryPop(out var seekData))
            {
                seeks.Clear();
                requiresBuffering = true;

                for (int i = 0; i < subNum; i++)
                {
                    // Display subtitles from cache when seeking while playing
                    bool display = false;
                    var cur = SubtitlesManager[i].GetCurrent();
                    if (cur != null)
                    {
                        if (!string.IsNullOrEmpty(cur.DisplayText))
                        {
                            SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                            display = true;
                        }
                        else if (cur.IsBitmap && cur.Bitmap != null)
                        {
                            SubtitleDisplay(cur.Bitmap, i);
                            display = true;
                        }

                        if (display)
                        {
                            sFramesPrev[i] = new SubtitlesFrame
                            {
                                timestamp = cur.StartTime.Ticks + Config.Subtitles[i].Delay,
                                duration = (uint)cur.Duration.TotalMilliseconds,
                                isTranslated = cur.UseTranslated
                            };
                        }
                    }

                    // clear subtitles
                    // but do not clear when cache hit
                    if (!display && sFramesPrev[i] != null)
                    {
                        sFramesPrev[i] = null;
                        SubtitleClear(i);
                    }
                }

                decoder.PauseDecoders(); // TBR: Required to avoid gettings packets between Seek and ShowFrame which causes resync issues

                if (decoder.Seek(seekData.accurate ? Math.Max(0, seekData.ms - (int)new TimeSpan(Config.Player.SeekAccurateFixMargin).TotalMilliseconds) : seekData.ms, seekData.forward, !seekData.accurate) < 0) // Consider using GetVideoFrame with no timestamp (any) to ensure keyframe packet for faster seek in HEVC
                    Log.Warn("Seek failed");
                else if (seekData.accurate)
                    decoder.GetVideoFrame(seekData.ms * (long)10000);
            }

            if (requiresBuffering)
            {
                if (VideoDemuxer.Interrupter.Timedout)
                    break;

                OnBufferingStarted();
                MediaBuffer();
                requiresBuffering = false;
                if (!seeks.IsEmpty)
                    continue;

                if (vFrame == null)
                {
                    if (decoderHasEnded)
                        OnBufferingCompleted();

                    Log.Warn("[MediaBuffer] No video frame");
                    break;
                }

                // Temp fix to ensure we had enough time to decode one more frame
                int retries = 5;
                while (IsPlaying && VideoDecoder.Frames.Count == 0 && retries-- > 0)
                    Thread.Sleep(10);

                // Give enough time for the 1st frame to be presented
                while (IsPlaying && DateTime.UtcNow.Ticks - showOneFrameTicks < VideoDecoder.VideoStream.FrameDuration)
                    Thread.Sleep(4);

                OnBufferingCompleted();

                audioBufferedDuration = 0;
                allowedLateAudioDrops = 7;
                elapsedSec = 0;
                startTicks = vFrame.timestamp;
                sw.Restart();
            }

            if (Status != Status.Playing)
                break;

            if (vFrame == null)
            {
                if (VideoDecoder.Status == MediaFramework.Status.Ended)
                {
                    if (!MainDemuxer.IsHLSLive)
                    {
                        if (Math.Abs(MainDemuxer.Duration - curTime) < 2 * VideoDemuxer.VideoStream.FrameDuration)
                            curTime = MainDemuxer.Duration;
                        else
                            curTime += VideoDemuxer.VideoStream.FrameDuration;

                        UI(() => Set(ref _CurTime, curTime, true, nameof(CurTime)));
                    }

                    break;
                }

                Log.Warn("No video frames");
                requiresBuffering = true;
                continue;
            }

            if (aFrame == null && !isAudioSwitch)
                AudioDecoder.Frames.TryDequeue(out aFrame);

            for (int i = 0; i < subNum; i++)
            {
                if (sFrames[i] == null && !isSubsSwitches[i])
                    SubtitlesDecoders[i].Frames.TryPeek(out sFrames[i]);
            }

            if (dFrame == null && !isDataSwitch)
                DataDecoder.Frames.TryPeek(out dFrame);

            elapsedTicks = (long) (sw.ElapsedTicks * SWFREQ_TO_TICKS); // Do we really need ticks precision?

            vDistanceMs =
                  (int) ((((vFrame.timestamp - startTicks) / speed) - elapsedTicks) / 10000);

            if (aFrame != null)
            {
                curAudioDeviceDelay = Audio.GetDeviceDelay();
                audioBufferedDuration = Audio.GetBufferedDuration();
                aDistanceMs = (int) ((((aFrame.timestamp - startTicks) / speed) - (elapsedTicks - curAudioDeviceDelay)) / 10000);

                // Try to keep the audio buffer full enough to avoid audio crackling (up to 50ms)
                while (audioBufferedDuration > 0 && audioBufferedDuration < 50 * 10000 && aDistanceMs > -5 && aDistanceMs < 50)
                {
                    Audio.AddSamples(aFrame);

                    if (isAudioSwitch)
                    {
                        audioBufferedDuration = 0;
                        aDistanceMs = int.MaxValue;
                        aFrame = null;
                    }
                    else
                    {
                        audioBufferedDuration = Audio.GetBufferedDuration();
                        AudioDecoder.Frames.TryDequeue(out aFrame);
                        if (aFrame != null)
                            aDistanceMs = (int) ((((aFrame.timestamp - startTicks) / speed) - (elapsedTicks - curAudioDeviceDelay)) / 10000);
                        else
                            aDistanceMs = int.MaxValue;
                    }
                }
            }
            else
                aDistanceMs = int.MaxValue;

            for (int i = 0; i < subNum; i++)
            {
                sDistanceMss[i] = sFrames[i] != null
                    ? (int)((((sFrames[i].timestamp - startTicks) / speed) - elapsedTicks) / 10000)
                    : int.MaxValue;
            }

            dDistanceMs = dFrame != null
                ? (int)((((dFrame.timestamp - startTicks) / speed) - elapsedTicks) / 10000)
                : int.MaxValue;

            sleepMs = Math.Min(vDistanceMs, aDistanceMs) - 1;
            if (sleepMs < 0 || sleepMs == int.MaxValue)
                sleepMs = 0;

            if (sleepMs > 2)
            {
                if (vDistanceMs > 2000)
                {
                    Log.Warn($"vDistanceMs = {vDistanceMs} (restarting)");
                    requiresBuffering = true;
                    continue;
                }

                if (Engine.Config.UICurTimePerSecond &&  (
                    (!MainDemuxer.IsHLSLive && curTime / 10000000 != _CurTime / 10000000) ||
                    (MainDemuxer.IsHLSLive && Math.Abs(elapsedTicks - elapsedSec) > 10000000)))
                {
                    elapsedSec  = elapsedTicks;
                    UI(() => UpdateCurTime());
                }

                Thread.Sleep(sleepMs);
            }

            if (aFrame != null) // Should use different thread for better accurancy (renderer might delay it on high fps) | also on high offset we will have silence between samples
            {
                if (Math.Abs(aDistanceMs - sleepMs) <= 5)
                {
                    Audio.AddSamples(aFrame);

                    // Audio Desync - Large Buffer | ASampleBytes (S16 * 2 Channels = 4) * TimeBase * 2 -frames duration-
                    if (Audio.GetBufferedDuration() > Math.Max(50 * 10000, (aFrame.dataLen / 4) * Audio.Timebase * 2))
                    {
                        if (CanDebug)
                            Log.Debug($"Audio desynced by {(int)(audioBufferedDuration / 10000)}ms, clearing buffers");

                        Audio.ClearBuffer();
                        audioBufferedDuration = 0;
                    }

                    aFrame = null;
                }
                else if (aDistanceMs > 4000) // Drops few audio frames in case of wrong timestamps (Note this should be lower, until swr has min/max samples for about 20-70ms)
                {
                    if (allowedLateAudioDrops > 0)
                    {
                        Audio.framesDropped++;
                        allowedLateAudioDrops--;
                        if (CanDebug) Log.Debug($"aDistanceMs 3 = {aDistanceMs}");
                        aFrame = null;
                        audioBufferedDuration = 0;
                    }
                }
                else if (aDistanceMs < -5) // Will be transfered back to decoder to drop invalid timestamps
                {
                    if (CanTrace) Log.Trace($"aDistanceMs = {aDistanceMs} | AudioFrames: {AudioDecoder.Frames.Count} AudioPackets: {AudioDecoder.Demuxer.AudioPackets.Count}");

                    if (GetBufferedDuration() < Config.Player.MinBufferDuration / 2)
                    {
                        if (CanInfo)
                            Log.Warn($"Not enough buffer (restarting)");

                        requiresBuffering = true;
                        continue;
                    }

                    audioBufferedDuration = 0;

                    if (aDistanceMs < -600)
                    {
                        if (CanTrace) Log.Trace($"All audio frames disposed");
                        Audio.framesDropped += AudioDecoder.Frames.Count;
                        AudioDecoder.DisposeFrames();
                        aFrame = null;
                    }
                    else
                    {
                        int maxdrop = Math.Max(Math.Min(vDistanceMs - sleepMs - 1, 20), 3);
                        for (int i=0; i<maxdrop; i++)
                        {
                            if (CanTrace) Log.Trace($"aDistanceMs 2 = {aDistanceMs}");
                            Audio.framesDropped++;
                            AudioDecoder.Frames.TryDequeue(out aFrame);

                            if (aFrame == null || ((aFrame.timestamp - startTicks) / speed) - ((long) (sw.ElapsedTicks * SWFREQ_TO_TICKS) - Audio.GetDeviceDelay() + 8 * 1000) > 0)
                                break;

                            aFrame = null;
                        }
                    }
                }
            }

            if (Math.Abs(vDistanceMs - sleepMs) <= 2)
            {
                if (CanTrace) Log.Trace($"[V] Presenting {TicksToTime(vFrame.timestamp)}");

                if (decoder.VideoDecoder.Renderer.Present(vFrame, false))
                    Video.framesDisplayed++;
                else
                    Video.framesDropped++;

                lock (seeks)
                    if (seeks.IsEmpty)
                    {
                        curTime = !MainDemuxer.IsHLSLive ? vFrame.timestamp : VideoDemuxer.CurTime;

                        if (Config.Player.UICurTimePerFrame)
                            UI(() => UpdateCurTime());
                    }

                VideoDecoder.Frames.TryDequeue(out vFrame);
                if (vFrame != null && Config.Player.MaxLatency != 0)
                    CheckLatency();
            }
            else if (vDistanceMs < -2)
            {
                if (vDistanceMs < -10 || GetBufferedDuration() < Config.Player.MinBufferDuration / 2)
                {
                    if (CanDebug)
                        Log.Debug($"vDistanceMs = {vDistanceMs} (restarting)");

                    requiresBuffering = true;
                    continue;
                }

                if (CanDebug)
                    Log.Debug($"vDistanceMs = {vDistanceMs}");

                Video.framesDropped++;
                VideoDecoder.DisposeFrame(vFrame);
                VideoDecoder.Frames.TryDequeue(out vFrame);
            }

            // Set the current time to SubtitleManager in a loop
            if (Config.Subtitles.Enabled)
            {
                for (int i = 0; i < subNum; i++)
                {
                    if (Subtitles[i].Enabled)
                    {
                        SubtitlesManager[i].SetCurrentTime(new TimeSpan(curTime));
                    }
                }
            }
            // Internal subtitles (Text or Bitmap or OCR)
            for (int i = 0; i < subNum; i++)
            {
                if (sFramesPrev[i] != null && ((sFramesPrev[i].timestamp - startTicks + (sFramesPrev[i].duration * (long)10000)) / speed) - (long) (sw.ElapsedTicks * SWFREQ_TO_TICKS) < 0)
                {
                    SubtitleClear(i);

                    sFramesPrev[i] = null;
                }

                if (sFrames[i] != null)
                {
                    if (Math.Abs(sDistanceMss[i] - sleepMs) < 30 || (sDistanceMss[i] < -30 && sFrames[i].duration + sDistanceMss[i] > 0))
                    {
                        if (sFrames[i].isBitmap && sFrames[i].sub.num_rects > 0)
                        {
                            if (SubtitlesSelectedHelper.GetMethod(i) == SelectSubMethod.OCR)
                            {
                                // Prevent the problem of OCR subtitles not being used in priority by setting
                                // the timestamp of the subtitles as they are displayed a little earlier
                                SubtitlesManager[i].SetCurrentTime(new TimeSpan(sFrames[i].timestamp));
                            }

                            var cur = SubtitlesManager[i].GetCurrent();

                            if (cur != null && !string.IsNullOrEmpty(cur.Text))
                            {
                                // Use OCR text subtitles if available
                                SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                            }
                            else
                            {
                                // renderer.CreateOverlayTexture(sFrame, SubtitlesDecoder.CodecCtx->width, SubtitlesDecoder.CodecCtx->height);
                                SubtitleDisplay(sFrames[i].bitmap, i);
                            }

                            SubtitlesDecoder.DisposeFrame(sFrames[i]);  // only rects
                        }
                        else if (sFrames[i].isBitmap && sFrames[i].sub.num_rects == 0)
                        {
                            // For Blu-ray subtitles (PGS), clear the previous subtitle
                            SubtitleClear(i);
                            sFramesPrev[i] = sFrames[i] = null;
                        }
                        else
                        {
                            // internal text sub (does not support translate)
                            SubtitleDisplay(sFrames[i].text, i, false);
                        }
                        sFramesPrev[i] = sFrames[i];
                        sFrames[i] = null;
                        SubtitlesDecoders[i].Frames.TryDequeue(out _);
                    }
                    else if (sDistanceMss[i] < -30)
                    {
                        if (CanDebug)
                            Log.Debug($"sDistanceMss[i] = {sDistanceMss[i]}");

                        //SubtitleClear(i);

                        // TODO: L: Here sFrames can be null, occurs when switching subtitles?
                        SubtitlesDecoder.DisposeFrame(sFrames[i]);
                        sFrames[i] = null;
                        SubtitlesDecoders[i].Frames.TryDequeue(out _);
                    }
                }
            }

            // External or ASR subtitles
            for (int i = 0; i < subNum; i++)
            {
                if (!Config.Subtitles.Enabled || !Subtitles[i].Enabled)
                {
                    continue;
                }

                SubtitleData cur = SubtitlesManager[i].GetCurrent();

                if (cur == null)
                {
                    continue;
                }

                if (sFramesPrev[i] == null ||
                    sFramesPrev[i].timestamp != cur.StartTime.Ticks + Config.Subtitles[i].Delay)
                {
                    bool display = false;
                    if (!string.IsNullOrEmpty(cur.DisplayText))
                    {
                        SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                        display = true;
                    }
                    else if (cur.IsBitmap && cur.Bitmap != null)
                    {
                        SubtitleDisplay(cur.Bitmap, i);
                        display = true;
                    }

                    if (display)
                    {
                        sFramesPrev[i] = new SubtitlesFrame
                        {
                            timestamp = cur.StartTime.Ticks + Config.Subtitles[i].Delay,
                            duration = (uint)cur.Duration.TotalMilliseconds,
                            isTranslated = cur.UseTranslated
                        };
                    }
                }
                else
                {
                    // Apply translation to current sub
                    if (Config.Subtitles[i].EnabledTranslated) {

                        // If the subtitle currently playing is not translated, change to the translated for display
                        if (sFramesPrev[i] != null &&
                            sFramesPrev[i].timestamp == cur.StartTime.Ticks + Config.Subtitles[i].Delay &&
                            sFramesPrev[i].isTranslated != cur.UseTranslated)
                        {
                            SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                            sFramesPrev[i].isTranslated = cur.UseTranslated;
                        }
                    }
                }
            }

            if (dFrame != null)
            {
                if (Math.Abs(dDistanceMs - sleepMs) < 30 || (dDistanceMs < -30))
                {
                    OnDataFrame?.Invoke(this, dFrame);

                    dFrame = null;
                    DataDecoder.Frames.TryDequeue(out var devnull);
                }
                else if (dDistanceMs < -30)
                {
                    if (CanDebug)
                        Log.Debug($"dDistanceMs = {dDistanceMs}");

                    dFrame = null;
                    DataDecoder.Frames.TryDequeue(out var devnull);
                }
            }
        }

        if (CanInfo) Log.Info($"Finished -> {TicksToTime(CurTime)}");
    }

    private void CheckLatency()
    {
        curLatency = GetBufferedDuration();

        if (CanDebug)
            Log.Debug($"[Latency {curLatency/10000}ms] Frames: {VideoDecoder.Frames.Count} Packets: {VideoDemuxer.VideoPackets.Count} Speed: {speed}");

        if (curLatency <= Config.Player.MinLatency) // We've reached the down limit (back to speed x1)
        {
            ChangeSpeedWithoutBuffering(1);
            return;
        }
        else if (curLatency < Config.Player.MaxLatency)
            return;

        var newSpeed = Math.Max(Math.Round((double)curLatency / Config.Player.MaxLatency, 1, MidpointRounding.ToPositiveInfinity), 1.1);

        if (newSpeed > 4) // TBR: dispose only as much as required to avoid rebuffering
        {
            decoder.Flush();
            requiresBuffering = true;
            Log.Debug($"[Latency {curLatency/10000}ms] Clearing queue");
            return;
        }

        ChangeSpeedWithoutBuffering(newSpeed);
    }
    private void ChangeSpeedWithoutBuffering(double newSpeed)
    {
        if (speed == newSpeed)
            return;

        long curTicks = DateTime.UtcNow.Ticks;

        if (newSpeed != 1 && curTicks - lastSpeedChangeTicks < Config.Player.LatencySpeedChangeInterval)
            return;

        lastSpeedChangeTicks = curTicks;

        if (CanDebug)
            Log.Debug($"[Latency {curLatency/10000}ms] Speed changed x{speed} -> x{newSpeed}");

        if (aFrame != null)
            AudioDecoder.FixSample(aFrame, speed, newSpeed);

        Speed       = newSpeed;
        requiresBuffering
                    = false;
        startTicks  = curTime;
        elapsedSec  = 0;
        sw.Restart();
    }
    private long GetBufferedDuration()
    {
        var decoder = VideoDecoder.Frames.IsEmpty ? 0 : VideoDecoder.Frames.ToArray()[^1].timestamp - vFrame.timestamp;
        var demuxer = VideoDemuxer.VideoPackets.IsEmpty || VideoDemuxer.VideoPackets.LastTimestamp == NoTs
            ? 0 :
            (VideoDemuxer.VideoPackets.LastTimestamp - VideoDemuxer.StartTime) - vFrame.timestamp;

        return Math.Max(decoder, demuxer);
    }

    private void AudioBuffer()
    {
        if (CanTrace) Log.Trace("Buffering");

        while ((isVideoSwitch || isAudioSwitch) && IsPlaying)
            Thread.Sleep(10);

        if (!IsPlaying)
            return;

        aFrame = null;
        Audio.ClearBuffer();
        decoder.AudioStream.Demuxer.Start();
        AudioDecoder.Start();

        while(AudioDecoder.Frames.IsEmpty && IsPlaying && AudioDecoder.IsRunning)
            Thread.Sleep(10);

        AudioDecoder.Frames.TryPeek(out aFrame);

        if (aFrame == null)
            return;

        lock (seeks)
            if (seeks.IsEmpty)
            {
                curTime = !MainDemuxer.IsHLSLive ? aFrame.timestamp : MainDemuxer.CurTime;
                UI(() =>
                {
                    Set(ref _CurTime, curTime, true, nameof(CurTime));
                    UpdateBufferedDuration();
                });
            }

        while(seeks.IsEmpty && decoder.AudioStream.Demuxer.BufferedDuration < Config.Player.MinBufferDuration && AudioDecoder.Frames.Count < Config.Decoder.MaxAudioFrames / 2 && IsPlaying && decoder.AudioStream.Demuxer.IsRunning && decoder.AudioStream.Demuxer.Status != MediaFramework.Status.QueueFull)
            Thread.Sleep(20);
    }
    private void ScreamerAudioOnly()
    {
        long bufferedDuration = 0;

        while (IsPlaying)
        {
            if (seeks.TryPop(out var seekData))
            {
                seeks.Clear();
                requiresBuffering = true;

                if (AudioDecoder.OnVideoDemuxer)
                {
                    if (decoder.Seek(seekData.ms, seekData.forward) < 0)
                        Log.Warn("Seek failed 1");
                }
                else
                {
                    if (decoder.SeekAudio(seekData.ms, seekData.forward) < 0)
                        Log.Warn("Seek failed 2");
                }
            }

            if (requiresBuffering)
            {
                OnBufferingStarted();
                AudioBuffer();
                requiresBuffering = false;

                if (!seeks.IsEmpty)
                    continue;

                if (!IsPlaying || AudioDecoder.Frames.IsEmpty)
                    break;

                OnBufferingCompleted();
            }

            if (AudioDecoder.Frames.IsEmpty)
            {
                if (bufferedDuration == 0)
                {
                    if (!IsPlaying || AudioDecoder.Status == MediaFramework.Status.Ended)
                        break;

                    Log.Warn("No audio frames");
                    requiresBuffering = true;
                }
                else
                {
                    Thread.Sleep(50); // waiting for audio buffer to be played before end
                    bufferedDuration = Audio.GetBufferedDuration();
                }

                continue;
            }

            // Support only ASR subtitle for audio
            foreach (int i in SubtitlesASR.SubIndexSet)
            {
                SubtitlesManager[i].SetCurrentTime(new TimeSpan(curTime));

                var cur = SubtitlesManager[i].GetCurrent();
                if (cur != null && !string.IsNullOrEmpty(cur.DisplayText))
                {
                    SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                }
                else
                {
                    SubtitleClear(i);
                }
            }

            bufferedDuration = Audio.GetBufferedDuration();

            if (bufferedDuration < 300 * 10000)
            {
                do
                {
                    AudioDecoder.Frames.TryDequeue(out aFrame);
                    if (aFrame == null || !IsPlaying)
                        break;

                    Audio.AddSamples(aFrame);
                    bufferedDuration += (long) ((aFrame.dataLen / 4) * Audio.Timebase);
                    curTime = !MainDemuxer.IsHLSLive ? aFrame.timestamp : MainDemuxer.CurTime;
                } while (bufferedDuration < 100 * 10000);

                lock (seeks)
                    if (seeks.IsEmpty)
                    {
                        if (!Engine.Config.UICurTimePerSecond || curTime / 10000000 != _CurTime / 10000000)
                        {
                            UI(() =>
                            {
                                Set(ref _CurTime, curTime, true, nameof(CurTime));
                                UpdateBufferedDuration();
                            });
                        }
                    }

                Thread.Sleep(20);
            }
            else
                Thread.Sleep(50);
        }
    }

    private void ScreamerReverse()
    {
        while (Status == Status.Playing)
        {
            if (seeks.TryPop(out var seekData))
            {
                seeks.Clear();
                if (decoder.Seek(seekData.ms, seekData.forward) < 0)
                    Log.Warn("Seek failed");
            }

            if (vFrame == null)
            {
                if (VideoDecoder.Status == MediaFramework.Status.Ended)
                    break;

                OnBufferingStarted();
                if (reversePlaybackResync)
                {
                    decoder.Flush();
                    VideoDemuxer.EnableReversePlayback(CurTime);
                    reversePlaybackResync = false;
                }
                VideoDemuxer.Start();
                VideoDecoder.Start();

                while (VideoDecoder.Frames.IsEmpty && Status == Status.Playing && VideoDecoder.IsRunning) Thread.Sleep(15);
                OnBufferingCompleted();
                VideoDecoder.Frames.TryDequeue(out vFrame);
                if (vFrame == null) { Log.Warn("No video frame"); break; }
                vFrame.timestamp = (long) (vFrame.timestamp / Speed);

                startTicks = vFrame.timestamp;
                sw.Restart();
                elapsedSec = 0;

                if (!MainDemuxer.IsHLSLive && seeks.IsEmpty)
                    curTime = (long) (vFrame.timestamp * Speed);
                UI(() => UpdateCurTime());
            }

            elapsedTicks    = startTicks - (long) (sw.ElapsedTicks * SWFREQ_TO_TICKS);
            vDistanceMs     = (int) ((elapsedTicks - vFrame.timestamp) / 10000);
            sleepMs         = vDistanceMs - 1;

            if (sleepMs < 0) sleepMs = 0;

            if (Math.Abs(vDistanceMs - sleepMs) > 5)
            {
                //Log($"vDistanceMs |-> {vDistanceMs}");
                VideoDecoder.DisposeFrame(vFrame);
                vFrame = null;
                Thread.Sleep(5);
                continue; // rebuffer
            }

            if (sleepMs > 2)
            {
                if (sleepMs > 1000)
                {
                    //Log($"sleepMs -> {sleepMs} , vDistanceMs |-> {vDistanceMs}");
                    VideoDecoder.DisposeFrame(vFrame);
                    vFrame = null;
                    Thread.Sleep(5);
                    continue; // rebuffer
                }

                // Every seconds informs the application with CurTime / Bitrates (invokes UI thread to ensure the updates will actually happen)
                if (Engine.Config.UICurTimePerSecond && (
                    (!MainDemuxer.IsHLSLive && curTime / 10000000 != _CurTime / 10000000) ||
                    (MainDemuxer.IsHLSLive && Math.Abs(elapsedTicks - elapsedSec) > 10000000)))
                {
                    elapsedSec  = elapsedTicks;
                    UI(() => UpdateCurTime());
                }

                Thread.Sleep(sleepMs);
            }

            decoder.VideoDecoder.Renderer.Present(vFrame, false);
            if (!MainDemuxer.IsHLSLive && seeks.IsEmpty)
            {
                curTime = (long) (vFrame.timestamp * Speed);

                if (Config.Player.UICurTimePerFrame)
                    UI(() => UpdateCurTime());
            }

            VideoDecoder.Frames.TryDequeue(out vFrame);
            if (vFrame != null)
                vFrame.timestamp = (long) (vFrame.timestamp / Speed);
        }
    }
}

public class BufferingCompletedArgs : EventArgs
{
    public string   Error       { get; }
    public bool     Success     { get; }

    public BufferingCompletedArgs(string error)
    {
        Error   = error;
        Success = Error == null;
    }
}

using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text;
using System.Xml.Serialization;
using FlyleafLib;
using LLPlayer.Extensions;

namespace LLPlayer.Services;

public class OpenSubtitlesProvider
{
    private readonly FlyleafManager FL;
    private readonly HttpClient _client;
    private string? _token;
    private bool _initialized = false;

    public OpenSubtitlesProvider(FlyleafManager fl)
    {
        FL = fl;
        HttpClient client = new();
        client.BaseAddress = new Uri("http://api.opensubtitles.org/xml-rpc");
        client.Timeout = TimeSpan.FromSeconds(15);

        _client = client;
    }

    private readonly SemaphoreSlim _loginSemaphore = new(1);

    private async Task Initialize()
    {
        if (!_initialized)
        {
            try
            {
                await _loginSemaphore.WaitAsync();
                await Login();
                _initialized = true;
            }
            finally
            {
                _loginSemaphore.Release();
            }
        }
    }

    /// <summary>
    /// Login
    /// </summary>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="HttpRequestException"></exception>
    private async Task Login()
    {
        string loginReqXml =
"""
<?xml version="1.0"?>
<methodCall>
  <methodName>LogIn</methodName>
  <params>
    <param>
      <value>
        <string/>
      </value>
    </param>
    <param>
      <value>
        <string/>
      </value>
    </param>
    <param>
      <value>en</value>
    </param>
    <param>
      <value>VLsub 0.10.2</value>
    </param>
  </params>
</methodCall>
""";

        var result = await _client.PostAsync(string.Empty, new StringContent(loginReqXml));
        var content = await result.Content.ReadAsStringAsync();

        var serializer = new XmlSerializer(typeof(MethodResponse));
        LoginResponse loginResponse = new();
        using (var reader = new StringReader(content))
        {
            MethodResponse? response = null;
            try
            {
                result.EnsureSuccessStatusCode();
                response = serializer.Deserialize(reader) as MethodResponse;
                if (response == null)
                {
                    throw new InvalidOperationException("response is not MethodResponse");
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Can't parse the login content: {ex.Message}", ex)
                {
                    Data =
                    {
                        ["status_code"] = ((int)result.StatusCode).ToString(),
                        ["login_content"] = content
                    }
                };
            }

            foreach (var member in response.Params.Param.Value.Struct.Member)
            {
                var propertyName = member.Name.ToUpperFirstChar();
                switch (propertyName)
                {
                    case nameof(loginResponse.Token):
                        loginResponse.Token = member.Value.String;
                        break;
                    case nameof(loginResponse.Status):
                        loginResponse.Status = member.Value.String;
                        break;
                }
            }
        }

        if (loginResponse.StatusCode != "200")
            throw new InvalidOperationException($"Can't login because status is '{loginResponse.StatusCode}'");

        if (string.IsNullOrWhiteSpace(loginResponse.Token))
            throw new InvalidOperationException("Can't login because token is empty");

        _token = loginResponse.Token;
    }

    /// <summary>
    /// Search
    /// </summary>
    /// <param name="query"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="HttpRequestException"></exception>
    public async Task<IList<SearchResponse>> Search(string query)
    {
        await Initialize();

        if (_token == null)
            throw new InvalidOperationException("token is not initialized");

        var subLanguageId = "all";
        var limit = 500;

        string searchReqXml =
$"""
<?xml version="1.0"?>
<methodCall>
  <methodName>SearchSubtitles</methodName>
  <params>
    <param>
      <value>{_token}</value>
    </param>
    <param>
      <value>
        <array>
          <data>
            <value>
              <struct>
                <member>
                  <name>query</name>
                  <value>{query}</value>
                </member>
                <member>
                  <name>sublanguageid</name>
                  <value>{subLanguageId}</value>
                </member>
              </struct>
            </value>
          </data>
        </array>
      </value>
    </param>
    <param>
      <value>
        <struct>
          <member>
            <name>limit</name>
            <value>
              <i4>{limit}</i4>
            </value>
          </member>
        </struct>
      </value>
    </param>
  </params>
</methodCall>
""";

        var result = await _client.PostAsync(string.Empty, new StringContent(searchReqXml));
        var content = await result.Content.ReadAsStringAsync();

        var serializer = new XmlSerializer(typeof(MethodResponse));

        List<SearchResponse> searchResponses = new();

        using (StringReader reader = new(content))
        {
            MethodResponse? response = null;
            try
            {
                result.EnsureSuccessStatusCode();
                response = serializer.Deserialize(reader) as MethodResponse;
                if (response == null)
                {
                    throw new InvalidOperationException("response is not MethodResponse");
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Can't parse the search content: {ex.Message}", ex)
                {
                    Data =
                    {
                        ["status_code"] = ((int)result.StatusCode).ToString(),
                        ["search_content"] = content
                    }
                };
            }

            if (!response.Params.Param.Value.Struct.Member.Any(m => m.Name == "status" && m.Value.String == "200 OK"))
                throw new InvalidOperationException("Can't get the search result, status is not 200.");

            var resultMembers = response.Params.Param.Value.Struct.Member.FirstOrDefault(m => m.Name == "data");
            if (resultMembers == null)
                throw new InvalidOperationException("Can't get the search result, data is not found.");

            foreach (var record in resultMembers.Value.Array.Data.Value)
            {
                SearchResponse searchResponse = new();
                foreach (var member in record.Struct.Member)
                {
                    var propertyName = member.Name.ToUpperFirstChar();

                    var property = typeof(SearchResponse).GetProperty(propertyName);
                    if (property != null && property.CanWrite)
                    {
                        switch (Type.GetTypeCode(property.PropertyType))
                        {
                            case TypeCode.Int32:
                                if (member.Value.String != null &&
                                    int.TryParse(member.Value.String, out var n))
                                {
                                    property.SetValue(searchResponse, n);
                                }
                                else
                                {
                                    property.SetValue(searchResponse, member.Value.Int);
                                }
                                break;

                            case TypeCode.Double:
                                if (member.Value.String != null &&
                                    double.TryParse(member.Value.String, out var d))
                                {
                                    property.SetValue(searchResponse, d);
                                }
                                else
                                {
                                    property.SetValue(searchResponse, member.Value.Double);
                                }
                                break;

                            case TypeCode.String:
                                property.SetValue(searchResponse, member.Value.String);
                                break;
                        }
                    }
                }

                searchResponses.Add(searchResponse);
            }
        }

        return searchResponses;
    }

    /// <summary>
    /// Download
    /// </summary>
    /// <param name="sub"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="HttpRequestException"></exception>
    public async Task<(byte[] data, bool isBitmap)> Download(SearchResponse sub)
    {
        await Initialize();

        if (_token == null)
            throw new InvalidOperationException("token is not initialized");

        var idSubtitleFile = sub.IDSubtitleFile;
        var isBitmap = sub.SubFormat.ToLower() == "sub";

        string downloadReqXml =
$"""
 <?xml version="1.0"?>
 <methodCall>
   <methodName>DownloadSubtitles</methodName>
   <params>
     <param>
       <value>{_token}</value>
     </param>
     <param>
       <value>
         <array>
           <data>
             <value>{idSubtitleFile}</value>
           </data>
         </array>
       </value>
     </param>
   </params>
 </methodCall>
 """;

        var result = await _client.PostAsync(string.Empty, new StringContent(downloadReqXml));
        var content = await result.Content.ReadAsStringAsync();

        XmlSerializer serializer = new(typeof(MethodResponse));

        DownloadResponse downloadResponse = new();

        using (StringReader reader = new(content))
        {
            MethodResponse? response = null;
            try
            {
                result.EnsureSuccessStatusCode();
                response = serializer.Deserialize(reader) as MethodResponse;
                if (response == null)
                {
                    throw new InvalidOperationException("response is not MethodResponse");
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Can't parse the download content: {ex.Message}", ex)
                {
                    Data =
                    {
                        ["status_code"] = ((int)result.StatusCode).ToString(),
                        ["download_content"] = content
                    }
                };
            }

            if (!response.Params.Param.Value.Struct.Member.Any(m => m.Name == "status" && m.Value.String == "200 OK"))
                throw new InvalidOperationException("Can't get the download result, status is not 200.");

            var resultMembers = response.Params.Param.Value.Struct.Member.FirstOrDefault(m => m.Name == "data");
            if (resultMembers == null)
                throw new InvalidOperationException("Can't get the download result, data is not found.");

            var data = resultMembers.Value.Array.Data.Value.First();

            foreach (var member in data.Struct.Member)
            {
                var propertyName = member.Name.ToUpperFirstChar();
                switch (propertyName)
                {
                    case nameof(downloadResponse.Data):
                        downloadResponse.Data = member.Value.String;
                        break;
                    case nameof(downloadResponse.Idsubtitlefile):
                        downloadResponse.Idsubtitlefile = member.Value.String;
                        break;
                }
            }
        }

        if (string.IsNullOrEmpty(downloadResponse.Data))
            throw new InvalidOperationException("Can't get the download result, base64 data is not found.");

        var gzipSub = Convert.FromBase64String(downloadResponse.Data);
        byte[]? subData;

        using MemoryStream compressedStream = new(gzipSub);
        using (GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress))
        using (MemoryStream resultStream = new())
        {
            gzipStream.CopyTo(resultStream);
            subData = resultStream.ToArray();
        }

        if (subData == null)
            throw new InvalidOperationException("Can't get the download result, decompressed data is not found.");

        if (isBitmap)
        {
            return (subData, true);
        }

        Encoding? encoding = TextEncodings.DetectEncoding(subData);
        encoding ??= Encoding.UTF8;

        // If there is originally a BOM, it will remain even if it is converted to a string, so delete it.
        string subString = encoding.GetString(subData).TrimStart('\uFEFF');

        // Convert to UTF-8 and return
        byte[] subText = Encoding.UTF8.GetBytes(subString);

        if (FL.Config.Subs.SubsExportUTF8WithBom)
        {
            // append BOM
            subText = Encoding.UTF8.GetPreamble().Concat(subText).ToArray();
        }

        return (subText, false);
    }
}

public class LoginResponse
{
    public string? Token { get; set; }
    public string? Status { get; set; }
    public string? StatusCode => Status?.Split(' ')[0];
}

public class SearchResponse
{
    public string IDSubtitleFile { get; set; } = "";
    public string SubFileName { get; set; } = "";
    public int SubSize { get; set; } // from string
    public string SubLastTS { get; set; } = "";
    public string IDSubtitle { get; set; } = "";
    public string SubLanguageID { get; set; } = "";
    public string SubFormat { get; set; } = "";
    public string SubAddDate { get; set; } = "";
    public double SubRating { get; set; } // from string
    public string SubSumVotes { get; set; } = "";
    public int SubDownloadsCnt { get; set; } // from string
    public string MovieName { get; set; } = "";
    public string MovieYear { get; set; } = "";
    public string MovieKind { get; set; } = "";
    public string ISO639 { get; set; } = "";
    public string LanguageName { get; set; } = "";
    public string SeriesSeason { get; set; } = "";
    public string SeriesEpisode { get; set; } = "";
    public string SubEncoding { get; set; } = "";
    public string SubDownloadLink { get; set; } = "";
    public string SubtitlesLink { get; set; } = "";
    public double Score { get; set; }
}

public class DownloadResponse
{
    public string? Data { get; set; }
    public string? Idsubtitlefile { get; set; }
}

[XmlRoot("value")]
public class Value
{
    [XmlElement("string")]
    public string? String { get; set; }

    [XmlElement("struct")]
    public Struct Struct { get; set; } = null!;

    [XmlElement("int")]
    public int Int { get; set; }

    [XmlElement("double")]
    public double Double { get; set; }

    [XmlElement("array")]
    public Array Array { get; set; } = null!;
}

[XmlRoot("member")]
public class Member
{
    [XmlElement("name")]
    public string Name { get; set; } = null!;

    [XmlElement("value")]
    public Value Value { get; set; } = null!;
}

[XmlRoot("struct")]
public class Struct
{
    [XmlElement("member")]
    public List<Member> Member { get; set; } = null!;
}

[XmlRoot("data")]
public class Data
{
    [XmlElement("value")]
    public List<Value> Value { get; set; } = null!;
}

[XmlRoot("array")]
public class Array
{
    [XmlElement("data")]
    public Data Data { get; set; } = null!;
}

[XmlRoot("param")]
public class Param
{
    [XmlElement("value")]
    public Value Value { get; set; } = null!;
}

[XmlRoot("params")]
public class Params
{
    [XmlElement("param")]
    public Param Param { get; set; } = null!;
}

[XmlRoot("methodResponse")]
public class MethodResponse
{
    [XmlElement("params")]
    public Params Params { get; set; } = null!;
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using FlyleafLib.MediaPlayer;
using LLPlayer.Converters;
using LLPlayer.Extensions;
using LLPlayer.Services;
using KeyBinding = FlyleafLib.MediaPlayer.KeyBinding;

namespace LLPlayer.Controls.Settings;

public partial class SettingsKeys : UserControl
{
    private SettingsKeysVM VM => (SettingsKeysVM)DataContext;

    public SettingsKeys()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsKeysVM>();
    }

    // Enable ComboBox to open when double-clicking a cell in Action
    private void ComboBox_Loaded(object sender, RoutedEventArgs e)
    {
        if (sender is ComboBox comboBox)
        {
            comboBox.IsDropDownOpen = true;
        }
    }

    // Scroll to the added record when a new record is added.
    private void KeyBindingsDataGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (VM.CmdLoad!.IsExecuting)
        {
            return;
        }

        if (KeyBindingsDataGrid.SelectedItem != null)
        {
            KeyBindingsDataGrid.UpdateLayout();
            KeyBindingsDataGrid.ScrollIntoView(KeyBindingsDataGrid.SelectedItem);
        }
    }
}

public class SettingsKeysVM : Bindable
{
    public FlyleafManager FL { get; }

    public SettingsKeysVM(FlyleafManager fl)
    {
        FL = fl;

        CmdLoad!.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == nameof(CmdLoad.IsExecuting))
            {
                OnPropertyChanged(nameof(CanApply));
            }
        };

        List<ActionData> mergeActions = new();

        foreach (KeyBindingAction action in Enum.GetValues<KeyBindingAction>())
        {
            if (action != KeyBindingAction.Custom)
            {
                mergeActions.Add(new ActionData()
                {
                    Action = action,
                    Description = action.GetDescription(),
                    DisplayName = action.ToString(),
                    Group = action.ToGroup()
                });
            }
        }

        foreach (CustomKeyBindingAction action in Enum.GetValues<CustomKeyBindingAction>())
        {
            mergeActions.Add(new ActionData()
            {
                Action = KeyBindingAction.Custom,
                CustomAction = action,
                Description = action.GetDescription(),
                DisplayName = action.ToString() + @" ©", // c=custom
                Group = action.ToGroup()
            });
        }

        mergeActions.Sort();

        Actions = mergeActions;

        // Grouping when displaying actions in ComboBox
        _actionsView = (ListCollectionView)CollectionViewSource.GetDefaultView(Actions);
        _actionsView.SortDescriptions.Add(new SortDescription(nameof(ActionData.Group), ListSortDirection.Ascending));
        _actionsView.SortDescriptions.Add(new SortDescription(nameof(ActionData.DisplayName), ListSortDirection.Ascending));

        _actionsView.GroupDescriptions!.Add(new PropertyGroupDescription(nameof(ActionData.Group)));
    }

    public List<ActionData> Actions { get; }
    private readonly ListCollectionView _actionsView;

    public ObservableCollection<KeyBindingWrapper> KeyBindings { get; } = new();

    public KeyBindingWrapper? SelectedKeyBinding { get; set => Set(ref field, value); }

    public int DuplicationCount
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanApply));
            }
        }
    }

    // Disable Apply button if there are duplicates or during loading
    public bool CanApply => DuplicationCount == 0 && !CmdLoad!.IsExecuting;

    public DelegateCommand? CmdAdd => field ??= new(() =>
    {
        KeyBindingWrapper added = new(new KeyBinding { Action = KeyBindingAction.AudioDelayAdd }, this);
        KeyBindings.Add(added);
        SelectedKeyBinding = added;

        added.ValidateShortcut();
    });

    public AsyncDelegateCommand? CmdLoad => field ??= new(async () =>
    {
        KeyBindings.Clear();
        DuplicationCount = 0;

        var keys = FL.PlayerConfig.Player.KeyBindings.Keys;

        var keyBindings = await Task.Run(() =>
        {
            List<KeyBindingWrapper> keyBindings = new(keys.Count);

            foreach (var k in keys)
            {
                try
                {
                    keyBindings.Add(new KeyBindingWrapper(k, this));
                }
                catch (SettingsPropertyNotFoundException)
                {
                    // ignored
                    // TODO: L: error handling
                    Debug.Fail("Weird custom key for settings?");
                }
            }

            return Task.FromResult(keyBindings);
        });

        foreach (var b in keyBindings)
        {
            KeyBindings.Add(b);
        }
    });

    /// <summary>
    /// Reflect customized key settings to Config.
    /// </summary>
    public DelegateCommand? CmdApply => field ??= new DelegateCommand(() =>
    {
        foreach (var b in KeyBindings)
        {
            Debug.Assert(!b.Duplicated, "Duplicate check not working");
        }

        var newKeys = KeyBindings.Select(k => k.ToKeyBinding()).ToList();

        foreach (var k in newKeys)
        {
            if (k.Action == KeyBindingAction.Custom)
            {
                if (!Enum.TryParse(k.ActionName, out CustomKeyBindingAction customAction))
                {
                    Guards.Fail();
                }
                k.SetAction(FL.Action.CustomActions[customAction], k.IsKeyUp);
            }
            else
            {
                k.SetAction(FL.PlayerConfig.Player.KeyBindings.GetKeyBindingAction(k.Action), k.IsKeyUp);
            }
        }

        FL.PlayerConfig.Player.KeyBindings.RemoveAll();
        FL.PlayerConfig.Player.KeyBindings.Keys = newKeys;

    }).ObservesCanExecute(() => CanApply);
}

public class ActionData : IComparable<ActionData>, IEquatable<ActionData>
{
    public string Description { get; set; } = null!;
    public string DisplayName { get; set; } = null!;
    public KeyBindingActionGroup Group { get; set; }
    public KeyBindingAction Action { get; set; }
    public CustomKeyBindingAction? CustomAction { get; set; }

    public int CompareTo(ActionData? other)
    {
        if (ReferenceEquals(this, other))
            return 0;
        if (other is null)
            return 1;
        return string.Compare(DisplayName, other.DisplayName, StringComparison.Ordinal);
    }

    public bool Equals(ActionData? other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        return Action == other.Action && CustomAction == other.CustomAction;
    }

    public override bool Equals(object? obj) => obj is ActionData o && Equals(o);

    public override int GetHashCode()
    {
        return HashCode.Combine((int)Action, CustomAction);
    }
}

public class KeyBindingWrapper : Bindable
{
    private readonly SettingsKeysVM _parent;

    /// <summary>
    /// constructor
    /// </summary>
    /// <param name="keyBinding"></param>
    /// <param name="parent"></param>
    /// <exception cref="SettingsPropertyNotFoundException"></exception>
    public KeyBindingWrapper(KeyBinding keyBinding, SettingsKeysVM parent)
    {
        ArgumentNullException.ThrowIfNull(parent);

        // TODO: L: performance issues when initializing?
        _parent = parent;

        Key = keyBinding.Key;

        ActionData action = new()
        {
            Action = keyBinding.Action,
        };

        if (keyBinding.Action != KeyBindingAction.Custom)
        {
            action.Description = keyBinding.Action.GetDescription();
            action.DisplayName = keyBinding.Action.ToString();
        }
        else if (Enum.TryParse(keyBinding.ActionName, out CustomKeyBindingAction customAction))
        {
            action.CustomAction = customAction;
            action.Description = customAction.GetDescription();
            action.DisplayName = keyBinding.ActionName + @" ©";
        }
        else
        {
            throw new SettingsPropertyNotFoundException($"Custom Action '{keyBinding.ActionName}' does not exist.");
        }

        Action = action;
        Alt = keyBinding.Alt;
        Ctrl = keyBinding.Ctrl;
        Shift = keyBinding.Shift;
        IsKeyUp = keyBinding.IsKeyUp;
        IsEnabled = keyBinding.IsEnabled;
    }

    public bool IsEnabled
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (!value)
                {
                    Duplicated = false;
                    DuplicatedInfo = null;
                }
                else
                {
                    ValidateShortcut();
                }

                ValidateKey(Key);
            }
        }
    }

    public bool Alt
    {
        get;
        set
        {
            if (Set(ref field, value) && IsEnabled)
            {
                ValidateShortcut();
                ValidateKey(Key);
            }
        }
    }

    public bool Ctrl
    {
        get;
        set
        {
            if (Set(ref field, value) && IsEnabled)
            {
                ValidateShortcut();
                ValidateKey(Key);
            }
        }
    }

    public bool Shift
    {
        get;
        set
        {
            if (Set(ref field, value) && IsEnabled)
            {
                ValidateShortcut();
                ValidateKey(Key);
            }
        }
    }

    public Key Key
    {
        get;
        set
        {
            Key prevKey = field;
            if (Set(ref field, value) && IsEnabled)
            {
                ValidateShortcut();
                ValidateKey(Key); // maybe don't need?
                // When the key is changed from A -> B, the state of A also needs to be updated.
                ValidateKey(prevKey);
            }
        }
    }

    public ActionData Action { get; set => Set(ref field, value); }

    public bool IsKeyUp { get; set => Set(ref field, value); }

    public bool Duplicated
    {
        get;
        private set
        {
            if (Set(ref field, value))
            {
                // Update duplicate counters held by the parent.
                _parent.DuplicationCount += value ? 1 : -1;
            }
        }
    }

    public string? DuplicatedInfo { get; private set => Set(ref field, value); }

    private bool IsSameKey(KeyBindingWrapper key)
    {
        return key.Key == Key && key.Alt == Alt && key.Shift == Shift && key.Ctrl == Ctrl;
    }

    private void ValidateKey(Key key)
    {
        foreach (var b in _parent.KeyBindings.Where(k => k.IsEnabled && k != this && k.Key == key))
        {
            b.ValidateShortcut();
        }
    }

    public KeyBindingWrapper Clone()
    {
        KeyBindingWrapper clone = (KeyBindingWrapper)MemberwiseClone();

        return clone;
    }

    /// <summary>
    /// Convert Wrapper to KeyBinding
    /// </summary>
    /// <returns></returns>
    public KeyBinding ToKeyBinding()
    {
        KeyBinding binding = new()
        {
            Key = Key,
            Action = Action.Action,
            Ctrl = Ctrl,
            Alt = Alt,
            Shift = Shift,
            IsKeyUp = IsKeyUp,
            IsEnabled = IsEnabled
        };

        if (Action.Action == KeyBindingAction.Custom)
        {
            binding.ActionName = Action.CustomAction.ToString();
        }

        return binding;
    }

    // TODO: L: This validation might be better done in the parent with event firing (I want to eliminate references to the parent).
    public void ValidateShortcut()
    {
        List<KeyBindingWrapper> sameKeys = _parent.KeyBindings
            .Where(k => k != this && k.IsEnabled && k.IsSameKey(this)).ToList();

        bool isDuplicate = sameKeys.Count > 0;

        UpdateDuplicated(isDuplicate);

        // Other records with the same key also update duplicate status
        foreach (KeyBindingWrapper b in sameKeys)
        {
            b.UpdateDuplicated(isDuplicate);
        }
    }

    private void UpdateDuplicated(bool duplicated)
    {
        Duplicated = duplicated;

        if (duplicated)
        {
            List<string> duplicateActions = _parent.KeyBindings
                        .Where(k => k != this && k.IsSameKey(this)).Select(k => k.Action.DisplayName)
                        .ToList();

            DuplicatedInfo = $"Key:{Key} is conflicted.\r\nAction:{string.Join(',', duplicateActions)} already uses.";
        }
        else
        {
            DuplicatedInfo = null;
        }
    }

    // TODO: L: Enable firing for multiple selections
    public DelegateCommand<KeyBindingWrapper> CmdDeleteRow => new((binding) =>
    {
        if (binding.Duplicated)
        {
            // Reduce duplicate counters of parents
            binding.Duplicated = false;
        }
        _parent.KeyBindings.Remove(binding);

        // Update other keys
        if (binding.IsEnabled)
        {
            ValidateKey(binding.Key);
        }
    });

    public DelegateCommand<Key?> CmdSetKey => new((key) =>
    {
        if (key.HasValue)
        {
            Key = key.Value;
        }
    });

    public DelegateCommand<KeyBindingWrapper> CmdCloneRow => new((keyBinding) =>
    {
        int index = _parent.KeyBindings.IndexOf(keyBinding);
        if (index != -1)
        {
            KeyBindingWrapper clone = Clone();

            _parent.KeyBindings.Insert(index + 1, clone);

            // Select added record
            _parent.SelectedKeyBinding = clone;

            // validate it
            clone.ValidateShortcut();
        }
    });
}

public class KeyCaptureTextBox : TextBox
{
    private static readonly HashSet<Key> IgnoreKeys =
    [
        Key.LeftShift,
        Key.RightShift,
        Key.LeftCtrl,
        Key.RightCtrl,
        Key.LeftAlt,
        Key.RightAlt,
        Key.LWin,
        Key.RWin,
        Key.CapsLock,
        Key.NumLock,
        Key.Scroll
    ];

    public static readonly DependencyProperty KeyProperty =
        DependencyProperty.Register(nameof(Key), typeof(Key), typeof(KeyCaptureTextBox),
            new FrameworkPropertyMetadata(Key.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Key Key
    {
        get => (Key)GetValue(KeyProperty);
        set => SetValue(KeyProperty, value);
    }

    public KeyCaptureTextBox()
    {
        Loaded += KeyCaptureTextBox_Loaded;
    }

    // Key input does not get focus when mouse clicked, so it gets it.
    private void KeyCaptureTextBox_Loaded(object sender, RoutedEventArgs e)
    {
        Focus();
        Keyboard.Focus(this);
    }

    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        if (IgnoreKeys.Contains(e.Key))
        {
            return;
        }

        // Press Enter to confirm.
        if (e.Key == Key.Enter)
        {
            // This would go to the right cell.
            //MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));

            // If the Enter key is pressed, it remains in place and the edit is confirmed
            var dataGrid = UIHelper.FindParent<DataGrid>(this);
            if (dataGrid != null)
            {
                dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
            }

            e.Handled = true; // Suppress default behavior (focus movement)
        }
        else
        {
            // Capture other key
            Key = e.Key;

            // Converts input key into human understandable name.
            if (KeyToStringConverter.KeyMappings.TryGetValue(e.Key, out string? keyName))
            {
                Text = keyName;
            }
            else
            {
                Text = e.Key.ToString();
            }
            e.Handled = true;
        }

        base.OnPreviewKeyDown(e);
    }
}

using System.Collections;
using System.Windows;
using System.Windows.Media;

namespace WpfColorFontDialog
{
    /// <summary>
    /// Interaction logic for ColorFontDialog.xaml
    /// </summary>
    public partial class ColorFontDialog : Window
    {
        private FontInfo _selectedFont;

        public FontInfo Font
        {
            get
            {
                return _selectedFont;
            }
            set
            {
                _selectedFont = value;
            }
        }

        private int[] _defaultFontSizes = { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72, 96 };
        private int[] _fontSizes = null;
        public int[] FontSizes
        {
            get
            {
                return _fontSizes ?? _defaultFontSizes;
            }
            set
            {
                _fontSizes = value;
            }
        }
        public ColorFontDialog(bool previewFontInFontList = true, bool allowArbitraryFontSizes = true, bool showColorPicker = true)
        {
            // Disable style inheritance from parents and apply standard styles
            InheritanceBehavior = InheritanceBehavior.SkipToThemeNext;

            InitializeComponent();
            I18NUtil.SetLanguage(Resources);
            this.colorFontChooser.PreviewFontInFontList = previewFontInFontList;
            this.colorFontChooser.AllowArbitraryFontSizes = allowArbitraryFontSizes;
            this.colorFontChooser.ShowColorPicker = showColorPicker;
        }

        private void btnOk_Click(object sender, RoutedEventArgs e)
        {
            this.Font = this.colorFontChooser.SelectedFont;
            base.DialogResult = new bool?(true);
        }

        private void SyncFontColor()
        {
            int colorIdx = AvailableColors.GetFontColorIndex(this.Font.Color);
            this.colorFontChooser.colorPicker.superCombo.SelectedIndex = colorIdx;
            this.colorFontChooser.txtSampleText.Foreground = this.Font.Color.Brush;
            this.colorFontChooser.colorPicker.superCombo.BringIntoView();
        }

        private void SyncFontName()
        {
            string fontFamilyName = this._selectedFont.Family.Source;
            bool foundMatch = false;
            int idx = 0;
            foreach (object item in (IEnumerable)this.colorFontChooser.lstFamily.Items)
            {
                if (fontFamilyName == item.ToString())
                {
                    foundMatch = true;
                    break;
                }
                idx++;
            }
            if (!foundMatch)
            {
                idx = 0;
            }
            this.colorFontChooser.lstFamily.SelectedIndex = idx;
            this.colorFontChooser.lstFamily.ScrollIntoView(this.colorFontChooser.lstFamily.Items[idx]);
        }

        private void SyncFontSize()
        {
            double fontSize = this._selectedFont.Size;
            this.colorFontChooser.lstFontSizes.ItemsSource = FontSizes;
            this.colorFontChooser.tbFontSize.Text = fontSize.ToString();
        }

        private void SyncFontTypeface()
        {
            string fontTypeFaceSb = FontInfo.TypefaceToString(this._selectedFont.Typeface);
            int idx = 0;
            foreach (object item in (IEnumerable)this.colorFontChooser.lstTypefaces.Items)
            {
                if (fontTypeFaceSb == FontInfo.TypefaceToString(item as FamilyTypeface))
                {
                    break;
                }
                idx++;
            }
            this.colorFontChooser.lstTypefaces.SelectedIndex = idx;
            this.colorFontChooser.lstTypefaces.ScrollIntoView(this.colorFontChooser.lstTypefaces.SelectedItem);
        }

        private void Window_Loaded_1(object sender, RoutedEventArgs e)
        {
            this.SyncFontColor();
            this.SyncFontName();
            this.SyncFontSize();
            this.SyncFontTypeface();
        }
    }
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Extensions;

// ref: https://stackoverflow.com/a/50905766/9070784
public static class TextBoxHelper
{
    #region Enum Declarations
    public enum NumericFormat
    {
        Double,
        Int,
        Uint,
        Natural
    }

    public enum EvenOddConstraint
    {
        All,
        OnlyEven,
        OnlyOdd
    }

    #endregion

    #region Dependency Properties & CLR Wrappers

    public static readonly DependencyProperty OnlyNumericProperty =
        DependencyProperty.RegisterAttached("OnlyNumeric", typeof(NumericFormat?), typeof(TextBoxHelper),
            new PropertyMetadata(null, DependencyPropertiesChanged));
    public static void SetOnlyNumeric(TextBox element, NumericFormat value) =>
        element.SetValue(OnlyNumericProperty, value);
    public static NumericFormat GetOnlyNumeric(TextBox element) =>
        (NumericFormat)element.GetValue(OnlyNumericProperty);

    public static readonly DependencyProperty DefaultValueProperty =
        DependencyProperty.RegisterAttached("DefaultValue", typeof(string), typeof(TextBoxHelper),
            new PropertyMetadata(null, DependencyPropertiesChanged));
    public static void SetDefaultValue(TextBox element, string value) =>
        element.SetValue(DefaultValueProperty, value);
    public static string GetDefaultValue(TextBox element) => (string)element.GetValue(DefaultValueProperty);

    public static readonly DependencyProperty EvenOddConstraintProperty =
        DependencyProperty.RegisterAttached("EvenOddConstraint", typeof(EvenOddConstraint), typeof(TextBoxHelper),
            new PropertyMetadata(EvenOddConstraint.All, DependencyPropertiesChanged));
    public static void SetEvenOddConstraint(TextBox element, EvenOddConstraint value) =>
        element.SetValue(EvenOddConstraintProperty, value);
    public static EvenOddConstraint GetEvenOddConstraint(TextBox element) =>
        (EvenOddConstraint)element.GetValue(EvenOddConstraintProperty);

    #endregion

    #region Dependency Properties Methods

    private static void DependencyPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is TextBox textBox))
            throw new Exception("Attached property must be used with TextBox.");

        switch (e.Property.Name)
        {
            case "OnlyNumeric":
                {
                    var castedValue = (NumericFormat?)e.NewValue;

                    if (castedValue.HasValue)
                    {
                        textBox.PreviewTextInput += TextBox_PreviewTextInput;
                        DataObject.AddPastingHandler(textBox, TextBox_PasteEventHandler);
                    }
                    else
                    {
                        textBox.PreviewTextInput -= TextBox_PreviewTextInput;
                        DataObject.RemovePastingHandler(textBox, TextBox_PasteEventHandler);
                    }

                    break;
                }

            case "DefaultValue":
                {
                    var castedValue = (string)e.NewValue;

                    if (castedValue != null)
                    {
                        textBox.TextChanged += TextBox_TextChanged;
                    }
                    else
                    {
                        textBox.TextChanged -= TextBox_TextChanged;
                    }

                    break;
                }
        }
    }

    #endregion

    private static void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        var textBox = (TextBox)sender;

        string newText;

        if (textBox.SelectionLength == 0)
        {
            newText = textBox.Text.Insert(textBox.SelectionStart, e.Text);
        }
        else
        {
            var textAfterDelete = textBox.Text.Remove(textBox.SelectionStart, textBox.SelectionLength);

            newText = textAfterDelete.Insert(textBox.SelectionStart, e.Text);
        }

        var evenOddConstraint = GetEvenOddConstraint(textBox);

        switch (GetOnlyNumeric(textBox))
        {
            case NumericFormat.Double:
                {
                    if (double.TryParse(newText, out double number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;
                        }
                    }
                    else
                        e.Handled = true;

                    break;
                }

            case NumericFormat.Int:
                {
                    if (int.TryParse(newText, out int number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;
                        }
                    }
                    else
                        e.Handled = true;

                    break;
                }

            case NumericFormat.Uint:
                {
                    if (uint.TryParse(newText, out uint number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    e.Handled = true;
                                else
                                    e.Handled = false;

                                break;
                        }
                    }
                    else
                        e.Handled = true;

                    break;
                }

            case NumericFormat.Natural:
                {
                    if (uint.TryParse(newText, out uint number))
                    {
                        if (number == 0)
                            e.Handled = true;
                        else
                        {
                            switch (evenOddConstraint)
                            {
                                case EvenOddConstraint.OnlyEven:

                                    if (number % 2 != 0)
                                        e.Handled = true;
                                    else
                                        e.Handled = false;

                                    break;

                                case EvenOddConstraint.OnlyOdd:

                                    if (number % 2 == 0)
                                        e.Handled = true;
                                    else
                                        e.Handled = false;

                                    break;
                            }
                        }
                    }
                    else
                        e.Handled = true;

                    break;
                }
        }
    }

    private static void TextBox_PasteEventHandler(object sender, DataObjectPastingEventArgs e)
    {
        var textBox = (TextBox)sender;

        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var clipboardText = (string)e.DataObject.GetData(typeof(string));

            var newText = textBox.Text.Insert(textBox.SelectionStart, clipboardText);

            var evenOddConstraint = GetEvenOddConstraint(textBox);

            switch (GetOnlyNumeric(textBox))
            {
                case NumericFormat.Double:
                    {
                        if (double.TryParse(newText, out double number))
                        {
                            switch (evenOddConstraint)
                            {
                                case EvenOddConstraint.OnlyEven:

                                    if (number % 2 != 0)
                                        e.CancelCommand();

                                    break;

                                case EvenOddConstraint.OnlyOdd:

                                    if (number % 2 == 0)
                                        e.CancelCommand();

                                    break;
                            }
                        }
                        else
                            e.CancelCommand();

                        break;
                    }

                case NumericFormat.Int:
                    {
                        if (int.TryParse(newText, out int number))
                        {
                            switch (evenOddConstraint)
                            {
                                case EvenOddConstraint.OnlyEven:

                                    if (number % 2 != 0)
                                        e.CancelCommand();

                                    break;

                                case EvenOddConstraint.OnlyOdd:

                                    if (number % 2 == 0)
                                        e.CancelCommand();

                                    break;
                            }
                        }
                        else
                            e.CancelCommand();

                        break;
                    }

                case NumericFormat.Uint:
                    {
                        if (uint.TryParse(newText, out uint number))
                        {
                            switch (evenOddConstraint)
                            {
                                case EvenOddConstraint.OnlyEven:

                                    if (number % 2 != 0)
                                        e.CancelCommand();

                                    break;

                                case EvenOddConstraint.OnlyOdd:

                                    if (number % 2 == 0)
                                        e.CancelCommand();

                                    break;
                            }
                        }
                        else
                            e.CancelCommand();

                        break;
                    }

                case NumericFormat.Natural:
                    {
                        if (uint.TryParse(newText, out uint number))
                        {
                            if (number == 0)
                                e.CancelCommand();
                            else
                            {
                                switch (evenOddConstraint)
                                {
                                    case EvenOddConstraint.OnlyEven:

                                        if (number % 2 != 0)
                                            e.CancelCommand();

                                        break;

                                    case EvenOddConstraint.OnlyOdd:

                                        if (number % 2 == 0)
                                            e.CancelCommand();

                                        break;
                                }
                            }
                        }
                        else
                        {
                            e.CancelCommand();
                        }

                        break;
                    }
            }
        }
        else
        {
            e.CancelCommand();
        }
    }

    private static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        var textBox = (TextBox)sender;

        var defaultValue = GetDefaultValue(textBox);

        var evenOddConstraint = GetEvenOddConstraint(textBox);

        switch (GetOnlyNumeric(textBox))
        {
            case NumericFormat.Double:
                {
                    if (double.TryParse(textBox.Text, out double number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    textBox.Text = defaultValue;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    textBox.Text = defaultValue;

                                break;
                        }
                    }
                    else
                        textBox.Text = defaultValue;

                    break;
                }

            case NumericFormat.Int:
                {
                    if (int.TryParse(textBox.Text, out int number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    textBox.Text = defaultValue;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    textBox.Text = defaultValue;

                                break;
                        }
                    }
                    else
                        textBox.Text = defaultValue;

                    break;
                }

            case NumericFormat.Uint:
                {
                    if (uint.TryParse(textBox.Text, out uint number))
                    {
                        switch (evenOddConstraint)
                        {
                            case EvenOddConstraint.OnlyEven:

                                if (number % 2 != 0)
                                    textBox.Text = defaultValue;

                                break;

                            case EvenOddConstraint.OnlyOdd:

                                if (number % 2 == 0)
                                    textBox.Text = defaultValue;

                                break;
                        }
                    }
                    else
                        textBox.Text = defaultValue;

                    break;
                }

            case NumericFormat.Natural:
                {
                    if (uint.TryParse(textBox.Text, out uint number))
                    {
                        if (number == 0)
                            textBox.Text = defaultValue;
                        else
                        {
                            switch (evenOddConstraint)
                            {
                                case EvenOddConstraint.OnlyEven:

                                    if (number % 2 != 0)
                                        textBox.Text = defaultValue;

                                    break;

                                case EvenOddConstraint.OnlyOdd:

                                    if (number % 2 == 0)
                                        textBox.Text = defaultValue;

                                    break;
                            }
                        }
                    }
                    else
                    {
                        textBox.Text = defaultValue;
                    }

                    break;
                }
        }
    }
}

using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace FlyleafLib.MediaFramework.MediaPlaylist;

public class M3UPlaylistItem
{
    public long     Duration    { get; set; }
    public string   Title       { get; set; }
    public string   OriginalTitle
                                { get; set; }
    public string   Url         { get; set; }
    public string   UserAgent   { get; set; }
    public string   Referrer    { get; set; }
    public bool     GeoBlocked  { get; set; }
    public bool     Not_24_7    { get; set; }
    public int      Height      { get; set; }

    public Dictionary<string, string> Tags { get; set; } = new Dictionary<string, string>();
}

public class M3UPlaylist
{
    public static List<M3UPlaylistItem> ParseFromHttp(string url, int timeoutMs = 30000)
    {
        string downStr = Utils.DownloadToString(url, timeoutMs);
        if (downStr == null)
            return new();

        using StringReader reader = new(downStr);
        return Parse(reader);
    }

    public static List<M3UPlaylistItem> ParseFromString(string text)
    {
        using StringReader reader = new(text);
        return Parse(reader);
    }

    public static List<M3UPlaylistItem> Parse(string filename)
    {
        using StreamReader reader = new(filename);
        return Parse(reader);
    }
    private static List<M3UPlaylistItem> Parse(TextReader reader)
    {
        string line;
        List<M3UPlaylistItem> items = new();

        while ((line = reader.ReadLine()) != null)
        {
            if (line.StartsWith("#EXTINF"))
            {
                M3UPlaylistItem item = new();
                var matches = Regex.Matches(line, " ([^\\s=]+)=\"([^\\s\"]+)\"");
                foreach (Match match in matches)
                {
                    if (match.Groups.Count == 3 && !string.IsNullOrWhiteSpace(match.Groups[2].Value))
                        item.Tags.Add(match.Groups[1].Value, match.Groups[2].Value);
                }

                item.Title = GetMatch(line, @",\s*(.*)$");
                item.OriginalTitle = item.Title;

                if (item.Title.IndexOf(" [Geo-blocked]") >= 0)
                {
                    item.GeoBlocked = true;
                    item.Title = item.Title.Replace(" [Geo-blocked]", "");
                }

                if (item.Title.IndexOf(" [Not 24/7]") >= 0)
                {
                    item.Not_24_7 = true;
                    item.Title = item.Title.Replace(" [Not 24/7]", "");
                }

                var height = Regex.Match(item.Title, " \\(([0-9]+)p\\)");
                if (height.Groups.Count == 2)
                {
                    item.Height = int.Parse(height.Groups[1].Value);
                    item.Title = item.Title.Replace(height.Groups[0].Value, "");
                }

                while ((line = reader.ReadLine()) != null && line.StartsWith("#EXTVLCOPT"))
                {
                    if (item.UserAgent == null)
                    {
                        item.UserAgent = GetMatch(line, "http-user-agent\\s*=\\s*\"*(.*)\"*");
                        if (item.UserAgent != null) continue;
                    }

                    if (item.Referrer == null)
                    {
                        item.Referrer = GetMatch(line, "http-referrer\\s*=\\s*\"*(.*)\"*");
                        if (item.Referrer != null) continue;
                    }
                }

                item.Url = line;
                items.Add(item);
            }

            // TODO: for m3u8 saved from windows media player
            //else if (!line.StartsWith("#"))
            //{
            //    M3UPlaylistItem item = new();
            //    item.Url = line.Trim(); // this can be relative path (base path from the root m3u8) in case of http(s) this can be another m3u8*
            //    if (item.Url.Length > 0)
            //    {
            //        item.Title = Path.GetFileName(item.Url);
            //        items.Add(item);
            //    }
            //}
        }

        return items;
    }

    private static string GetMatch(string text, string pattern)
    {
        var match = Regex.Match(text, pattern);
        return match.Success && match.Groups.Count > 1 ? match.Groups[1].Value : null;
    }
}

using System.Globalization;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using FlyleafLib;
using static FlyleafLib.MediaFramework.MediaDemuxer.Demuxer;

namespace LLPlayer.Converters;

[ValueConversion(typeof(int), typeof(Qualities))]
public class QualityToLevelsConverter : IValueConverter
{
    public enum Qualities
    {
        None,
        Low,
        Med,
        High,
        _4k
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        int videoHeight = (int)value;

        if (videoHeight > 1080)
            return Qualities._4k;
        if (videoHeight > 720)
            return Qualities.High;
        if (videoHeight == 720)
            return Qualities.Med;

        return Qualities.Low;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}

[ValueConversion(typeof(int), typeof(Volumes))]
public class VolumeToLevelsConverter : IValueConverter
{
    public enum Volumes
    {
        Mute,
        Low,
        Med,
        High
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        int volume = (int)value;

        if (volume == 0)
            return Volumes.Mute;
        if (volume > 99)
            return Volumes.High;
        if (volume > 49)
            return Volumes.Med;
        return Volumes.Low;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}

public class SumConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double sum = 0;

        if (values == null)
            return sum;

        foreach (object value in values)
        {
            if (value == DependencyProperty.UnsetValue)
                continue;
            sum += (double)value;
        }

        return sum;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        => throw new NotImplementedException();
}

public class PlaylistItemsConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return $"Playlist ({values[0]}/{values[1]})";
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        => throw new NotImplementedException();
}

[ValueConversion(typeof(Color), typeof(string))]
public class ColorToHexConverter : IValueConverter
{
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is null)
            return null;

        string UpperHexString(int i) => i.ToString("X2").ToUpper();
        Color color = (Color)value;
        string hex = UpperHexString(color.R) +
                      UpperHexString(color.G) +
                      UpperHexString(color.B);
        return hex;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            return ColorConverter.ConvertFromString("#" + value.ToString());
        }
        catch (Exception)
        {
            // ignored
        }

        return Binding.DoNothing;
    }
}

public class Tuple3Converter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return (values[0], values[1], values[2]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class SubIndexToIsEnabledConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Any(x => x == DependencyProperty.UnsetValue))
            return DependencyProperty.UnsetValue;

        // values[0] = Tag (index)
        // values[1] = IsPrimaryEnabled
        // values[2] = IsSecondaryEnabled
        int index = int.Parse((string)values[0]);
        bool isPrimaryEnabled = (bool)values[1];
        bool isSecondaryEnabled = (bool)values[2];

        return index == 0 ? isPrimaryEnabled : isSecondaryEnabled;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(IEnumerable<Chapter>), typeof(DoubleCollection))]
public class ChaptersToTicksConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is IEnumerable<Chapter> chapters)
        {
            // Convert tick count to seconds
            List<double> secs = chapters.Select(c => c.StartTime / 10000000.0).ToList();
            if (secs.Count <= 1)
            {
                return Binding.DoNothing;
            }

            return new DoubleCollection(secs);
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(IEnumerable<Chapter>), typeof(TickPlacement))]
public class ChaptersToTickPlacementConverter : IValueConverter
{
    public TickPlacement TickPlacement { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is IEnumerable<Chapter> chapters && chapters.Count() >= 2)
        {
            return TickPlacement;
        }

        return TickPlacement.None;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class AspectRatioIsCheckedConverter : IMultiValueConverter
{
    // values[0]: SelectedAspectRatio, values[1]: current AspectRatio
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length < 2)
        {
            return false;
        }

        if (values[0] is AspectRatio selected && values[1] is AspectRatio current)
        {
            return selected.Equals(current);
        }
        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

using FluentAssertions;
using FluentAssertions.Execution;

namespace FlyleafLib.MediaPlayer;

public class SubManagerTests
{
    private readonly SubManager _subManager;

    public SubManagerTests()
    {
        SubManager subManager = new(new Config(true), 0, false);
        List<SubtitleData> subsData =
        [
            new() { StartTime = TimeSpan.FromSeconds(1), EndTime = TimeSpan.FromSeconds(5), Text = "1. Hello World!" },
            new() { StartTime = TimeSpan.FromSeconds(10), EndTime = TimeSpan.FromSeconds(15), Text = "2. How are you" },
            new() { StartTime = TimeSpan.FromSeconds(20), EndTime = TimeSpan.FromSeconds(25), Text = "3. I'm fine" },
            new() { StartTime = TimeSpan.FromSeconds(28), EndTime = TimeSpan.FromSeconds(29), Text = "4. Thank you" },
            new() { StartTime = TimeSpan.FromSeconds(30), EndTime = TimeSpan.FromSeconds(35), Text = "5. Good bye" }
        ];

        subManager.Load(subsData);

        _subManager = subManager;
    }

    #region Seek
    [Fact]
    public void SubManagerTest_First_Yet()
    {
        // before the first subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(0.5);
        _subManager.SetCurrentTime(currentTime);

        // 1. Hello World!
        var nextIndex = 0;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.First);
            _subManager.CurrentIndex.Should().Be(-1);

            _subManager.GetCurrent().Should().BeNull();
            _subManager.GetPrev().Should().BeNull();
            _subManager.GetNext().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[nextIndex].Text);
        }
    }

    [Fact]
    public void SubManagerTest_First_Showing()
    {
        // During playback of the first subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(1);
        _subManager.SetCurrentTime(currentTime);

        // 1. Hello World!
        var curIndex = 0;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.Showing);
            _subManager.CurrentIndex.Should().Be(curIndex);

            _subManager.GetCurrent().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex].Text);
            _subManager.GetPrev().Should().BeNull();
            _subManager.GetNext().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex + 1].Text);
        }
    }

    [Fact]
    public void SubManagerTest_Middle_Showing()
    {
        // During playback of the middle subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(22);
        _subManager.SetCurrentTime(currentTime);

        // 3. I'm fine
        var curIndex = 2;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.Showing);
            _subManager.CurrentIndex.Should().Be(curIndex);

            _subManager.GetCurrent().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex].Text);
            _subManager.GetPrev().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex - 1].Text);
            _subManager.GetNext().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex + 1].Text);
        }
    }

    [Fact]
    public void SubManagerTest_Middle_Yet()
    {
        // just before the middle subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(18);
        _subManager.SetCurrentTime(currentTime);

        // 3. I'm fine
        var nextIndex = 2;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.Around);
            _subManager.CurrentIndex.Should().Be(nextIndex - 1);

            // Seek falls back to PrevSeek so we can seek, this class returns null
            _subManager.GetCurrent().Should().BeNull();
            _subManager.GetPrev().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[nextIndex - 1].Text);
            _subManager.GetNext().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[nextIndex].Text);
        }
    }

    [Fact]
    public void SubManagerTest_Last_Showing()
    {
        // During playback of the last subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(35);
        _subManager.SetCurrentTime(currentTime);

        // 5. Good bye
        var curIndex = 4;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.Showing);
            _subManager.CurrentIndex.Should().Be(curIndex);

            _subManager.GetCurrent().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex].Text);
            _subManager.GetPrev().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[curIndex - 1].Text);
            _subManager.GetNext().Should().BeNull();
        }
    }

    [Fact]
    public void SubManagerTest_Last_Past()
    {
        // after the last subtitle
        TimeSpan currentTime = TimeSpan.FromSeconds(99);
        _subManager.SetCurrentTime(currentTime);

        // 5. Good bye
        var prevIndex = 4;

        var subsData = _subManager.Subs;

        using (new AssertionScope())
        {
            _subManager.State.Should().Be(SubManager.PositionState.Last);
            // OK?
            _subManager.CurrentIndex.Should().Be(prevIndex);

            // Seek falls back to PrevSeek so we can seek, this returns null
            _subManager.GetCurrent().Should().BeNull();
            _subManager.GetPrev().Should()
                .NotBeNull().And
                .Match<SubtitleData>(s => s.Text == subsData[prevIndex].Text);
            _subManager.GetNext().Should().BeNull();
        }
    }
    #endregion

    #region DeleteAfter
    [Fact]
    public void SubManagerTest_DeleteAfter_DeleteFromMiddle()
    {
        _subManager.DeleteAfter(TimeSpan.FromSeconds(20));

        using (new AssertionScope())
        {
            _subManager.Subs.Count.Should().Be(2);
            _subManager.Subs.Select(s => s.Text!.Substring(0, 1))
                .Should().BeEquivalentTo(["1", "2"]);
        }
    }

    [Fact]
    public void SubManagerTest_DeleteAfter_DeleteAll()
    {
        _subManager.DeleteAfter(TimeSpan.FromSeconds(3));

        _subManager.Subs.Count.Should().Be(0);
    }

    [Fact]
    public void SubManagerTest_DeleteAfter_DeleteLast()
    {
        _subManager.DeleteAfter(TimeSpan.FromSeconds(32));

        using (new AssertionScope())
        {
            _subManager.Subs.Count.Should().Be(4);
            _subManager.Subs.Select(s => s.Text!.Substring(0, 1))
                .Should().BeEquivalentTo(["1", "2", "3", "4"]);
        }
    }

    [Fact]
    public void SubManagerTest_DeleteAfter_NoDelete()
    {
        _subManager.DeleteAfter(TimeSpan.FromSeconds(36));

        _subManager.Subs.Count.Should().Be(5);
    }
    #endregion
}

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using CliWrap.Builders;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
using LLPlayer.Views;
using Whisper.net.LibraryLoader;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitlesASR : UserControl
{
    public SettingsSubtitlesASR()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsSubtitlesASRVM>();
    }
}

public class SettingsSubtitlesASRVM : Bindable
{
    public FlyleafManager FL { get; }
    private readonly IDialogService _dialogService;

    public SettingsSubtitlesASRVM(FlyleafManager fl, IDialogService dialogService)
    {
        FL = fl;
        _dialogService = dialogService;

        LoadDownloadedModels();

        foreach (RuntimeLibrary library in FL.PlayerConfig.Subtitles.WhisperCppConfig.RuntimeLibraries)
        {
            SelectedLibraries.Add(library);
        }

        foreach (RuntimeLibrary library in Enum.GetValues<RuntimeLibrary>().Where(l => l != RuntimeLibrary.CoreML))
        {
            if (!SelectedLibraries.Contains(library))
            {
                AvailableLibraries.Add(library);
            }
        }
        SelectedLibraries.CollectionChanged += SelectedLibrariesOnCollectionChanged;

        foreach (WhisperLanguage lang in WhisperLanguage.GetWhisperLanguages())
        {
            WhisperLanguages.Add(lang);
        }

        ActiveEngineTabNo = (int)FL.PlayerConfig.Subtitles.ASREngine;
    }

    public int ActiveEngineTabNo { get; }

    public ObservableCollection<WhisperLanguage> WhisperLanguages { get; } = new();

    #region whisper.cpp
    public ObservableCollection<RuntimeLibrary> AvailableLibraries { get; } = new();
    public ObservableCollection<RuntimeLibrary> SelectedLibraries { get; } = new();

    public RuntimeLibrary? SelectedAvailable
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanMoveRight));
            }
        }
    }

    public RuntimeLibrary? SelectedSelected
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                OnPropertyChanged(nameof(CanMoveLeft));
                OnPropertyChanged(nameof(CanMoveUp));
                OnPropertyChanged(nameof(CanMoveDown));
            }
        }
    }

    public bool CanMoveRight => SelectedAvailable.HasValue;
    public bool CanMoveLeft => SelectedSelected.HasValue;
    public bool CanMoveUp => SelectedSelected.HasValue && SelectedLibraries.IndexOf(SelectedSelected.Value) > 0;
    public bool CanMoveDown => SelectedSelected.HasValue && SelectedLibraries.IndexOf(SelectedSelected.Value) < SelectedLibraries.Count - 1;

    private void SelectedLibrariesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        // Apply to config
        FL.PlayerConfig.Subtitles.WhisperCppConfig.RuntimeLibraries = [.. SelectedLibraries];
    }

    private void LoadDownloadedModels()
    {
        WhisperCppModel? prevSelected = FL.PlayerConfig.Subtitles.WhisperCppConfig.Model;
        FL.PlayerConfig.Subtitles.WhisperCppConfig.Model = null;
        DownloadModels.Clear();

        List<WhisperCppModel> models = WhisperCppModelLoader.LoadDownloadedModels();
        foreach (var model in models)
        {
            DownloadModels.Add(model);
        }

        FL.PlayerConfig.Subtitles.WhisperCppConfig.Model = DownloadModels.FirstOrDefault(m => m.Equals(prevSelected));

        if (FL.PlayerConfig.Subtitles.WhisperCppConfig.Model == null && DownloadModels.Count == 1)
        {
            // automatically set first downloaded model
            FL.PlayerConfig.Subtitles.WhisperCppConfig.Model = DownloadModels.First();
        }
    }

    public ObservableCollection<WhisperCppModel> DownloadModels { get; set => Set(ref field, value); } = new();

    public OrderedDictionary<string, string> PromptPresets { get; } = new()
    {
        ["Use Chinese (Simplified), with punctuation"] = "以下是普通话的句子。",
        ["Use Chinese (Traditional), with punctuation"] = "以下是普通話的句子。"
    };

    public KeyValuePair<string, string>? SelectedPromptPreset
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (value.HasValue)
                {
                    FL.PlayerConfig.Subtitles.WhisperCppConfig.Prompt = value.Value.Value;
                }
            }
        }
    }

    public DelegateCommand? CmdDownloadModel => field ??= new(() =>
    {
        _dialogService.ShowDialog(nameof(WhisperModelDownloadDialog));

        LoadDownloadedModels();
    });

    public DelegateCommand? CmdMoveRight => field ??= new DelegateCommand(() =>
    {
        if (SelectedAvailable.HasValue && !SelectedLibraries.Contains(SelectedAvailable.Value))
        {
            SelectedLibraries.Add(SelectedAvailable.Value);
            AvailableLibraries.Remove(SelectedAvailable.Value);
        }
    }).ObservesCanExecute(() => CanMoveRight);

    public DelegateCommand? CmdMoveLeft => field ??= new DelegateCommand(() =>
    {
        if (SelectedSelected.HasValue)
        {
            AvailableLibraries.Add(SelectedSelected.Value);
            SelectedLibraries.Remove(SelectedSelected.Value);
        }
    }).ObservesCanExecute(() => CanMoveLeft);

    public DelegateCommand? CmdMoveUp => field ??= new DelegateCommand(() =>
    {
        int index = SelectedLibraries.IndexOf(SelectedSelected!.Value);
        if (index > 0)
        {
            SelectedLibraries.Move(index, index - 1);
            OnPropertyChanged(nameof(CanMoveUp));
            OnPropertyChanged(nameof(CanMoveDown));
        }
    }).ObservesCanExecute(() => CanMoveUp);

    public DelegateCommand? CmdMoveDown => field ??= new DelegateCommand(() =>
    {
        int index = SelectedLibraries.IndexOf(SelectedSelected!.Value);
        if (index < SelectedLibraries.Count - 1)
        {
            SelectedLibraries.Move(index, index + 1);
            OnPropertyChanged(nameof(CanMoveUp));
            OnPropertyChanged(nameof(CanMoveDown));
        }
    }).ObservesCanExecute(() => CanMoveDown);
    #endregion

    #region faster-whisper
    public OrderedDictionary<string, string> ExtraArgumentsPresets { get; } = new()
    {
        ["Use CUDA device"] = "--device cuda",
        ["Use CUDA second device"] = "--device cuda:1",
        ["Use CPU device"] = "--device cpu",
        ["Use Chinese (Simplified), with punctuation"] = "--initial_prompt \"以下是普通话的句子。\"",
        ["Use Chinese (Traditional), with punctuation"] = "--initial_prompt \"以下是普通話的句子。\""
    };

    public KeyValuePair<string, string>? SelectedExtraArgumentsPreset
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (value.HasValue)
                {
                    FL.PlayerConfig.Subtitles.FasterWhisperConfig.ExtraArguments = value.Value.Value;
                }
            }
        }
    }

    public DelegateCommand? CmdDownloadEngine => field ??= new(() =>
    {
        _dialogService.ShowDialog(nameof(WhisperEngineDownloadDialog));

        // update binding of downloaded state forcefully
        var prev = FL.PlayerConfig.Subtitles.ASREngine;
        FL.PlayerConfig.Subtitles.ASREngine = SubASREngineType.WhisperCpp;
        FL.PlayerConfig.Subtitles.ASREngine = prev;
    });

    public DelegateCommand? CmdOpenModelFolder => field ??= new(() =>
    {
        if (!Directory.Exists(WhisperConfig.ModelsDirectory))
            return;

        Process.Start(new ProcessStartInfo
        {
            FileName = WhisperConfig.ModelsDirectory,
            UseShellExecute = true,
            CreateNoWindow = true
        });
    });

    public DelegateCommand? CmdCopyDebugCommand => field ??= new(() =>
    {
        var cmdBase = FasterWhisperASRService.BuildCommand(FL.PlayerConfig.Subtitles.FasterWhisperConfig,
            FL.PlayerConfig.Subtitles.WhisperConfig);

        var sampleWavePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "kennedy.wav");
        ArgumentsBuilder args = new();
        args.Add(sampleWavePath);

        string addedArgs = args.Build();

        var cmd = cmdBase.WithArguments($"{cmdBase.Arguments} {addedArgs}");
        Clipboard.SetText(cmd.CommandToText());
    });

    public DelegateCommand? CmdCopyHelpCommand => field ??= new(() =>
    {
        var cmdBase = FasterWhisperASRService.BuildCommand(FL.PlayerConfig.Subtitles.FasterWhisperConfig,
            FL.PlayerConfig.Subtitles.WhisperConfig);

        var cmd = cmdBase.WithArguments("--help");
        Clipboard.SetText(cmd.CommandToText());
    });
#endregion
}

[ValueConversion(typeof(Enum), typeof(bool))]
public class ASREngineDownloadedConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is SubASREngineType engine)
        {
            if (engine == SubASREngineType.FasterWhisper)
            {
                if (File.Exists(FasterWhisperConfig.DefaultEnginePath))
                {
                    return true;
                }
            }
        }

        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using LLPlayer.Services;

namespace LLPlayer.Converters;
[ValueConversion(typeof(bool), typeof(bool))]
public class InvertBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool boolValue)
        {
            return !boolValue;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool boolValue)
        {
            return !boolValue;
        }
        return false;
    }
}

[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityMiscConverter : IValueConverter
{
    public Visibility FalseVisibility { get; set; } = Visibility.Collapsed;
    public bool Invert { get; set; } = false;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (Invert)
        {
            return (bool)value ? FalseVisibility : Visibility.Visible;
        }

        return (bool)value ? Visibility.Visible : FalseVisibility;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

[ValueConversion(typeof(double), typeof(double))]
public class DoubleToPercentageConverter : IValueConverter
{
    // Model → View (0.0–1.0 → 0–100)
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is double d)
            return Math.Round(d * 100.0, 0);
        return 0.0;
    }

    // View → Model (0–100 → 0.0–1.0)
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is double d)
            return ToDouble(d);

        if (value is string sd)
        {
            if (double.TryParse(sd, out d))
            {
                if (d < 0)
                    d = 0;
                else if (d > 100)
                    d = 100;
            }

            return ToDouble(d);
        }

        return 0.0;

        static double ToDouble(double value)
        {
            return Math.Max(0.0, Math.Min(1.0, Math.Round(value / 100.0, 2)));
        }
    }
}

[ValueConversion(typeof(Enum), typeof(string))]
public class EnumToStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string str;
        try
        {
            str = Enum.GetName(value.GetType(), value)!;
            return str;
        }
        catch
        {
            return string.Empty;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

[ValueConversion(typeof(Enum), typeof(string))]
public class EnumToDescriptionConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Enum enumValue)
        {
            return enumValue.GetDescription();
        }
        return value.ToString() ?? "";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(Enum), typeof(bool))]
public class EnumToBooleanConverter : IValueConverter
{
    // value, parameter = Enum
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is not Enum enumValue || parameter is not Enum enumTarget)
            return false;

        return enumValue.Equals(enumTarget);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return parameter;
    }
}

[ValueConversion(typeof(Enum), typeof(Visibility))]
public class EnumToVisibilityConverter : IValueConverter
{
    // value, parameter = Enum
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is not Enum enumValue || parameter is not Enum enumTarget)
            return false;

        return enumValue.Equals(enumTarget) ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(Color), typeof(Brush))]
public class ColorToBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Color color)
        {
            return new SolidColorBrush(color);
        }
        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is SolidColorBrush brush)
        {
            return brush.Color;
        }
        return default(Color);
    }
}

/// <summary>
/// Converts from System.Windows.Input.Key to human readable string
/// </summary>
[ValueConversion(typeof(Key), typeof(string))]
public class KeyToStringConverter : IValueConverter
{
    public static readonly Dictionary<Key, string> KeyMappings = new()
    {
        { Key.D0, "0" },
        { Key.D1, "1" },
        { Key.D2, "2" },
        { Key.D3, "3" },
        { Key.D4, "4" },
        { Key.D5, "5" },
        { Key.D6, "6" },
        { Key.D7, "7" },
        { Key.D8, "8" },
        { Key.D9, "9" },
        { Key.Prior, "PageUp" },
        { Key.Next, "PageDown" },
        { Key.Return, "Enter" },
        { Key.Oem1, ";" },
        { Key.Oem2, "/" },
        { Key.Oem3, "`" },
        { Key.Oem4, "[" },
        { Key.Oem5, "\\" },
        { Key.Oem6, "]" },
        { Key.Oem7, "'" },
        { Key.OemPlus, "Plus" },
        { Key.OemMinus, "Minus" },
        { Key.OemComma, "," },
        { Key.OemPeriod, "." }
    };

    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is Key key)
        {
            if (KeyMappings.TryGetValue(key, out var mappedValue))
            {
                return mappedValue;
            }

            return key.ToString();
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(long), typeof(string))]
public class FileSizeHumanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is long size)
        {
            return FormatBytes(size);
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    private static string FormatBytes(long bytes)
    {
        string[] sizes = ["B", "KB", "MB", "GB"];
        double len = bytes;
        int order = 0;
        while (len >= 1024 && order < sizes.Length - 1)
        {
            order++;
            len /= 1024;
        }
        return $"{len:0.##} {sizes[order]}";
    }
}

[ValueConversion(typeof(int?), typeof(string))]
public class NullableIntConverter : IValueConverter
{
    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return value is int intValue ? intValue.ToString() : string.Empty;
    }

    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        string? str = value as string;
        if (string.IsNullOrWhiteSpace(str))
            return null;

        if (int.TryParse(str, out int result))
            return result;

        return null;
    }
}

using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

namespace FlyleafLib.MediaFramework.MediaPlaylist;

public class PLSPlaylist
{
    [DllImport("kernel32")]
    private static extern long WritePrivateProfileString(string name, string key, string val, string filePath);
    [DllImport("kernel32")]
    private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, int size, string filePath);

    public string path;

    public static List<PLSPlaylistItem> Parse(string filename)
    {
        List<PLSPlaylistItem> items = new();
        string res;
        int entries = 1000;

        if ((res = GetINIAttribute("playlist", "NumberOfEntries", filename)) != null)
            entries = int.Parse(res);

        for (int i=1; i<=entries; i++)
        {
            if ((res = GetINIAttribute("playlist", $"File{i}", filename)) == null)
                break;

            PLSPlaylistItem item = new() { Url = res };

            if ((res = GetINIAttribute("playlist", $"Title{i}", filename)) != null)
                item.Title = res;

            if ((res = GetINIAttribute("playlist", $"Length{i}", filename)) != null)
                item.Duration = int.Parse(res);

            items.Add(item);
        }

        return items;
    }

    public static string GetINIAttribute(string name, string key, string path)
    {
        StringBuilder sb = new(255);
        return GetPrivateProfileString(name, key, "", sb, 255, path) > 0
            ? sb.ToString() : null;
    }
}

public class PLSPlaylistItem
{
    public int      Duration    { get; set; }
    public string   Title       { get; set; }
    public string   Url         { get; set; }
}

using System.Windows;
using System.Windows.Controls;
using FlyleafLib.MediaPlayer;
using LLPlayer.Services;

namespace LLPlayer.Controls;

public partial class SubtitlesControl : UserControl
{
    public FlyleafManager FL { get; }

    public SubtitlesControl()
    {
        InitializeComponent();

        FL = ((App)Application.Current).Container.Resolve<FlyleafManager>();

        DataContext = this;
    }

    private void SubtitlePanel_OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (e.HeightChanged)
        {
            // Sometimes there is a very small difference in decimal points when the subtitles are switched, and this event fires.
            // If you update the margin of the Sub at this time, the window will go wrong, so do it only when the difference is above a certain level.
            double heightDiff = Math.Abs(e.NewSize.Height - e.PreviousSize.Height);
            if (heightDiff >= 1.0)
            {
                FL.Config.Subs.SubsPanelSize = e.NewSize;
            }
        }
    }

    private async void SelectableSubtitleText_OnWordClicked(object sender, WordClickedEventArgs e)
    {
        await WordPopupControl.OnWordClicked(e);
    }

    private void SelectableSubtitleText_OnWordClickedDown(object? sender, EventArgs e)
    {
        // Assume drag and stop playback.
        if (FL.Player.Status == Status.Playing)
        {
            FL.Player.Pause();
        }
    }
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using Windows.Media.Ocr;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;
using LLPlayer.Views;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitlesOCR : UserControl
{
    public SettingsSubtitlesOCR()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsSubtitlesOCRVM>();
    }
}

public class SettingsSubtitlesOCRVM : Bindable
{
    public FlyleafManager FL { get; }
    private readonly IDialogService _dialogService;

    public SettingsSubtitlesOCRVM(FlyleafManager fl, IDialogService dialogService)
    {
        FL = fl;
        _dialogService = dialogService;

        LoadDownloadedTessModels();
        LoadAvailableMsModels();
    }

    public ObservableCollection<TesseractModel> DownloadedTessLanguages { get; } = new();
    public ObservableCollection<TessOcrLanguageGroup> TessLanguageGroups { get; } = new();

    public ObservableCollection<Windows.Globalization.Language> AvailableMsOcrLanguages { get; } = new();
    public ObservableCollection<MsOcrLanguageGroup> MsLanguageGroups { get; } = new();

    [field: AllowNull, MaybeNull]
    public DelegateCommand CmdDownloadTessModel => field ??= new(() =>
    {
        _dialogService.ShowDialog(nameof(TesseractDownloadDialog));

        // reload
        LoadDownloadedTessModels();
    });

    private void LoadDownloadedTessModels()
    {
        DownloadedTessLanguages.Clear();

        List<TesseractModel> langs = TesseractModelLoader.LoadDownloadedModels().ToList();
        foreach (var lang in langs)
        {
            DownloadedTessLanguages.Add(lang);
        }

        TessLanguageGroups.Clear();

        List<TessOcrLanguageGroup> langGroups = langs
            .GroupBy(l => l.ISO6391)
            .Where(lg => lg.Count() >= 2)
            .Select(lg => new TessOcrLanguageGroup
            {
                ISO6391 = lg.Key,
                Members = new ObservableCollection<TessOcrLanguageGroupMember>(
                    lg.Select(m => new TessOcrLanguageGroupMember()
                    {
                        DisplayName = m.Lang.ToString(),
                        LangCode = m.LangCode
                    })
                )
            }).ToList();

        Dictionary<string, string>? regionConfig = FL.PlayerConfig.Subtitles.TesseractOcrRegions;
        foreach (TessOcrLanguageGroup group in langGroups)
        {
            // Load preferred region settings from config
            if (regionConfig != null && regionConfig.TryGetValue(group.ISO6391, out string? code))
            {
                group.SelectedMember = group.Members.FirstOrDefault(m => m.LangCode == code);
            }
            group.PropertyChanged += TessLanguageGroup_OnPropertyChanged;

            TessLanguageGroups.Add(group);
        }
    }

    private void LoadAvailableMsModels()
    {
        var langs = OcrEngine.AvailableRecognizerLanguages.ToList();
        foreach (var lang in langs)
        {
            AvailableMsOcrLanguages.Add(lang);
        }

        List<MsOcrLanguageGroup> langGroups = langs
            .GroupBy(l => l.LanguageTag.Split('-').First())
            .Where(lg => lg.Count() >= 2)
            .Select(lg => new MsOcrLanguageGroup
            {
                ISO6391 = lg.Key,
                Members = new ObservableCollection<MsOcrLanguageGroupMember>(
                    lg.Select(m => new MsOcrLanguageGroupMember()
                    {
                        DisplayName = m.DisplayName,
                        LanguageTag = m.LanguageTag,
                        NativeName = m.NativeName
                    })
                )
            }).ToList();

        Dictionary<string, string>? regionConfig = FL.PlayerConfig.Subtitles.MsOcrRegions;
        foreach (MsOcrLanguageGroup group in langGroups)
        {
            // Load preferred region settings from config
            if (regionConfig != null && regionConfig.TryGetValue(group.ISO6391, out string? tag))
            {
                group.SelectedMember = group.Members.FirstOrDefault(m => m.LanguageTag == tag);
            }
            group.PropertyChanged += MsLanguageGroup_OnPropertyChanged;

            MsLanguageGroups.Add(group);
        }
    }

    private void MsLanguageGroup_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(MsOcrLanguageGroup.SelectedMember))
        {
            Dictionary<string, string> iso6391ToTag = new();

            foreach (MsOcrLanguageGroup group in MsLanguageGroups)
            {
                if (group.SelectedMember != null)
                {
                    iso6391ToTag.Add(group.ISO6391, group.SelectedMember.LanguageTag);
                }
            }

            FL.PlayerConfig.Subtitles.MsOcrRegions = iso6391ToTag;
        }
    }

    private void TessLanguageGroup_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(TessOcrLanguageGroup.SelectedMember))
        {
            Dictionary<string, string> iso6391ToCode = new();

            foreach (TessOcrLanguageGroup group in TessLanguageGroups)
            {
                if (group.SelectedMember != null)
                {
                    iso6391ToCode.Add(group.ISO6391, group.SelectedMember.LangCode);
                }
            }

            FL.PlayerConfig.Subtitles.TesseractOcrRegions = iso6391ToCode;
        }
    }
}

#region Tesseract
public class TessOcrLanguageGroupMember : Bindable
{
    public required string LangCode { get; init; }
    public required string DisplayName { get; init; }
}

public class TessOcrLanguageGroup : Bindable
{
    public required string ISO6391 { get; init; }

    public string DisplayName
    {
        get
        {
            var lang = Language.Get(ISO6391);
            return lang.TopEnglishName;
        }
    }

    public required ObservableCollection<TessOcrLanguageGroupMember> Members { get; init; }
    public TessOcrLanguageGroupMember? SelectedMember { get; set => Set(ref field, value); }
}
#endregion

#region Microsoft OCR
public class MsOcrLanguageGroupMember : Bindable
{
    public required string LanguageTag { get; init; }
    public required string DisplayName { get; init; }
    public required string NativeName { get; init; }
}

public class MsOcrLanguageGroup : Bindable
{
    public required string ISO6391 { get; init; }

    public string DisplayName
    {
        get
        {
            var lang = Language.Get(ISO6391);
            return lang.TopEnglishName;
        }
    }

    public required ObservableCollection<MsOcrLanguageGroupMember> Members { get; init; }
    public MsOcrLanguageGroupMember? SelectedMember { get; set => Set(ref field, value); }
}
#endregion

using System.Runtime.InteropServices;
using System.Threading;

using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaFramework.MediaFrame;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDecoder;

public unsafe partial class AudioDecoder
{
    AVFilterContext*        abufferCtx;
    AVFilterContext*        abufferSinkCtx;
    AVFilterGraph*          filterGraph;
    bool                    abufferDrained;
    long                    curSamples;
    double                  missedSamples;
    long                    filterFirstPts;
    bool                    setFirstPts;
    object                  lockSpeed = new();
    AVRational              sinkTimebase;
    AVFrame*                filtframe;

    private AVFilterContext* CreateFilter(string name, string args, AVFilterContext* prevCtx = null, string id = null)
    {
        int ret;
        AVFilterContext*    filterCtx;
        AVFilter*           filter;

        id ??= name;

        filter  = avfilter_get_by_name(name);
        if (filter == null)
            throw new Exception($"[Filter {name}] not found");

        ret     = avfilter_graph_create_filter(&filterCtx, filter, id, args, null, filterGraph);
        if (ret < 0)
            throw new Exception($"[Filter {name}] avfilter_graph_create_filter failed ({FFmpegEngine.ErrorCodeToMsg(ret)})");

        if (prevCtx == null)
            return filterCtx;

        ret     = avfilter_link(prevCtx, 0, filterCtx, 0);

        return ret != 0
            ? throw new Exception($"[Filter {name}] avfilter_link failed ({FFmpegEngine.ErrorCodeToMsg(ret)})")
            : filterCtx;
    }
    private int SetupFilters()
    {
        int ret = -1;

        try
        {
            DisposeFilters();

            AVFilterContext* linkCtx;

            sinkTimebase    = new() { Num = 1, Den = codecCtx->sample_rate};
            filtframe       = av_frame_alloc();
            filterGraph     = avfilter_graph_alloc();
            setFirstPts     = true;
            abufferDrained  = false;

            // IN (abuffersrc)
            linkCtx = abufferCtx = CreateFilter("abuffer",
                $"channel_layout={AudioStream.ChannelLayoutStr}:sample_fmt={AudioStream.SampleFormatStr}:sample_rate={codecCtx->sample_rate}:time_base={sinkTimebase.Num}/{sinkTimebase.Den}");

            // USER DEFINED
            if (Config.Audio.Filters != null)
                foreach (var filter in Config.Audio.Filters)
                    try
                    {
                        linkCtx = CreateFilter(filter.Name, filter.Args, linkCtx, filter.Id);
                    }
                    catch (Exception e) { Log.Error($"{e.Message}"); }

            // SPEED (atempo up to 3) | [0.125 - 0.25](3), [0.25 - 0.5](2), [0.5 - 2.0](1), [2.0 - 4.0](2), [4.0 - X](3)
            if (speed != 1)
            {
                if (speed >= 0.5 && speed <= 2)
                    linkCtx = CreateFilter("atempo", $"tempo={speed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                else if ((speed > 2 & speed <= 4) || (speed >= 0.25 && speed < 0.5))
                {
                    var singleAtempoSpeed = Math.Sqrt(speed);
                    linkCtx = CreateFilter("atempo", $"tempo={singleAtempoSpeed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                    linkCtx = CreateFilter("atempo", $"tempo={singleAtempoSpeed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                }
                else if (speed > 4 || speed >= 0.125 && speed < 0.25)
                {
                    var singleAtempoSpeed = Math.Pow(speed, 1.0 / 3);
                    linkCtx = CreateFilter("atempo", $"tempo={singleAtempoSpeed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                    linkCtx = CreateFilter("atempo", $"tempo={singleAtempoSpeed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                    linkCtx = CreateFilter("atempo", $"tempo={singleAtempoSpeed.ToString("0.0000000000", System.Globalization.CultureInfo.InvariantCulture)}", linkCtx);
                }
            }

            // OUT (abuffersink)
            abufferSinkCtx = CreateFilter("abuffersink", null, null);

            int tmpSampleRate = AudioStream.SampleRate;
            fixed (AVSampleFormat* ptr = &AOutSampleFormat)
                ret = av_opt_set_bin(abufferSinkCtx , "sample_fmts"         , (byte*)ptr,            sizeof(AVSampleFormat) , OptSearchFlags.Children);
            ret = av_opt_set_bin(abufferSinkCtx     , "sample_rates"        , (byte*)&tmpSampleRate, sizeof(int)            , OptSearchFlags.Children);
            ret = av_opt_set_int(abufferSinkCtx     , "all_channel_counts"  , 0                                             , OptSearchFlags.Children);
            ret = av_opt_set(abufferSinkCtx         , "ch_layouts"          , "stereo"                                      , OptSearchFlags.Children);
            avfilter_link(linkCtx, 0, abufferSinkCtx, 0);

            // GRAPH CONFIG
            ret = avfilter_graph_config(filterGraph, null);

            // CRIT TBR:!!!
            var tb = 1000 * 10000.0 / sinkTimebase.Den; // Ensures we have at least 20-70ms samples to avoid audio crackling and av sync issues
            ((FilterLink*)abufferSinkCtx->inputs[0])->min_samples = (int) (20 * 10000 / tb);
            ((FilterLink*)abufferSinkCtx->inputs[0])->max_samples = (int) (70 * 10000 / tb);

            return ret < 0
                ? throw new Exception($"[FilterGraph] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})")
                : 0;
        }
        catch (Exception e)
        {
            fixed(AVFilterGraph** filterGraphPtr = &filterGraph)
                avfilter_graph_free(filterGraphPtr);

            Log.Error($"{e.Message}");

            return ret;
        }
    }

    private void DisposeFilters()
    {
        if (filterGraph == null)
            return;

        fixed(AVFilterGraph** filterGraphPtr = &filterGraph)
            avfilter_graph_free(filterGraphPtr);

        if (filtframe != null)
            fixed (AVFrame** ptr = &filtframe)
                av_frame_free(ptr);

        abufferCtx      = null;
        abufferSinkCtx  = null;
        filterGraph     = null;
        filtframe       = null;
    }
    protected override void OnSpeedChanged(double value)
    {
        // Possible Task to avoid locking UI thread as lockAtempo can wait for the Frames queue to be freed (will cause other issues and couldnt reproduce the possible dead lock)
        cBufTimesCur = cBufTimesSize;
        lock (lockSpeed)
        {
            if (filterGraph != null)
                DrainFilters();

            cBufTimesCur= 1;
            oldSpeed    = speed;
            speed       = value;

            var frames = Frames.ToArray();
            for (int i = 0; i < frames.Length; i++)
                FixSample(frames[i], oldSpeed, speed);

            if (filterGraph != null)
                SetupFilters();
        }
    }
    internal void FixSample(AudioFrame frame, double oldSpeed, double speed)
    {
        var oldDataLen = frame.dataLen;
        frame.dataLen = Utils.Align((int) (oldDataLen * oldSpeed / speed), ASampleBytes);
        fixed (byte* cBufStartPosPtr = &cBuf[0])
        {
            var curOffset = (long)frame.dataPtr - (long)cBufStartPosPtr;

            if (speed < oldSpeed)
            {
                if (curOffset + frame.dataLen >= cBuf.Length)
                {
                    frame.dataPtr = (IntPtr)cBufStartPosPtr;
                    curOffset  = 0;
                    oldDataLen = 0;
                }

                // fill silence
                for (int p = oldDataLen; p < frame.dataLen; p++)
                    cBuf[curOffset + p] = 0;
            }
        }
    }
    private int UpdateFilterInternal(string filterId, string key, string value)
    {
        int ret = avfilter_graph_send_command(filterGraph, filterId, key, value, null, 0, 0);
        Log.Info($"[{filterId}] {key}={value} {(ret >=0 ? "success" : "failed")}");

        return ret;
    }
    internal int SetupFiltersOrSwr()
    {
        lock (lockSpeed)
        {
            int ret = -1;

            if (Disposed)
                return ret;

            if (Config.Audio.FiltersEnabled)
            {
                ret = SetupFilters();

                if (ret != 0)
                {
                    Log.Error($"Setup filters failed. Fallback to Swr.");
                    ret = SetupSwr();
                }
                else
                    DisposeSwr();
            }
            else
            {
                DisposeFilters();
                ret = SetupSwr();
            }

            return ret;
        }
    }

    public int UpdateFilter(string filterId, string key, string value)
    {
        lock (lockCodecCtx)
            return filterGraph != null ? UpdateFilterInternal(filterId, key, value) : -1;
    }
    public int ReloadFilters()
    {
        if (!Config.Audio.FiltersEnabled)
            return -1;

        lock (lockActions)
            lock (lockCodecCtx)
                return SetupFilters();
    }

    private void ProcessFilters()
    {
        if (setFirstPts)
        {
            setFirstPts     = false;
            filterFirstPts  = frame->pts;
            curSamples      = 0;
            missedSamples   = 0;
        }
        else if (Math.Abs(frame->pts - nextPts) > 10 * 10000) // 10ms distance should resync filters (TBR: it should be 0ms however we might get 0 pkt_duration for unknown?)
        {
            DrainFilters();
            Log.Warn($"Resync filters! ({Utils.TicksToTime((long)((frame->pts - nextPts) * AudioStream.Timebase))} distance)");
            //resyncWithVideoRequired = !VideoDecoder.Disposed;
            DisposeFrames();
            avcodec_flush_buffers(codecCtx);
            if (filterGraph != null)
                SetupFilters();
            return;
        }

        nextPts = frame->pts + frame->duration;

        int ret;

        if ((ret = av_buffersrc_add_frame_flags(abufferCtx, frame, AVBuffersrcFlag.KeepRef | AVBuffersrcFlag.NoCheckFormat)) < 0) // AV_BUFFERSRC_FLAG_KEEP_REF = 8, AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT = 1 (we check format change manually before here)
        {
            Log.Warn($"[buffersrc] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");
            Status = Status.Stopping;
            return;
        }

        while (true)
        {
            if ((ret = av_buffersink_get_frame_flags(abufferSinkCtx, filtframe, 0)) < 0) // Sometimes we get AccessViolationException while we UpdateFilter (possible related with .NET7 debug only bug)
                return; // EAGAIN (Some filters will send EAGAIN even if EOF currently we handled cause our Status will be Draining)

            if (filtframe->pts == AV_NOPTS_VALUE) // we might desync here (we dont count frames->nb_samples) ?
            {
                av_frame_unref(filtframe);
                continue;
            }

            ProcessFilter();

            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxAudioFrames * cBufTimesCur)
            {
                Monitor.Exit(lockCodecCtx);
                lock (lockStatus)
                    if (Status == Status.Running)
                        Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxAudioFrames * cBufTimesCur && (Status == Status.QueueFull || Status == Status.Draining))
                    Thread.Sleep(20);

                Monitor.Enter(lockCodecCtx);

                lock (lockStatus)
                {
                    if (Status == Status.QueueFull)
                        Status = Status.Running;
                    else if (Status != Status.Draining)
                        return;
                }
            }
        }
    }
    private void DrainFilters()
    {
        if (abufferDrained)
            return;

        abufferDrained = true;

        int ret;

        if ((ret = av_buffersrc_add_frame(abufferCtx, null)) < 0)
        {
            Log.Warn($"[buffersrc] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");
            return;
        }

        while (true)
        {
            if ((ret = av_buffersink_get_frame_flags(abufferSinkCtx, filtframe, 0)) < 0)
                return;

            if (filtframe->pts == AV_NOPTS_VALUE)
            {
                av_frame_unref(filtframe);
                return;
            }

            ProcessFilter();
        }
    }
    private void ProcessFilter()
    {
        var curLen = filtframe->nb_samples * ASampleBytes;

        if (filtframe->nb_samples > cBufSamples) // (min 10000)
            AllocateCircularBuffer(filtframe->nb_samples);
        else if (cBufPos + curLen >= cBuf.Length)
            cBufPos = 0;

        long newPts         = filterFirstPts + av_rescale_q((long)(curSamples + missedSamples), sinkTimebase, AudioStream.AVStream->time_base);
        var samplesSpeed1   = filtframe->nb_samples * speed;
        missedSamples      += samplesSpeed1 - (int)samplesSpeed1;
        curSamples         += (int)samplesSpeed1;

        AudioFrame mFrame = new()
        {
            dataLen         = curLen,
            timestamp       = (long)((newPts * AudioStream.Timebase) - demuxer.StartTime + Config.Audio.Delay)
        };

        if (CanTrace) Log.Trace($"Processes {Utils.TicksToTime(mFrame.timestamp)}");

        fixed (byte* circularBufferPosPtr = &cBuf[cBufPos])
            mFrame.dataPtr = (IntPtr)circularBufferPosPtr;

        Marshal.Copy((IntPtr) filtframe->data[0], cBuf, cBufPos, mFrame.dataLen);
        cBufPos += curLen;

        Frames.Enqueue(mFrame);
        av_frame_unref(filtframe);
    }
}

/// <summary>
/// FFmpeg Filter
/// </summary>
public class Filter
{
    /// <summary>
    /// <para>
    /// FFmpeg valid filter id
    /// (Required only to send commands)
    /// </para>
    /// </summary>
    public string Id    { get; set; }

    /// <summary>
    /// FFmpeg valid filter name
    /// </summary>
    public string Name  { get; set; }

    /// <summary>
    /// FFmpeg valid filter args
    /// </summary>
    public string Args  { get; set; }
}

using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Imaging;
using FlyleafLib.MediaPlayer;
using LLPlayer.Services;

namespace LLPlayer.Converters;

public class WidthPercentageMultiConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] is double actualWidth &&
            values[1] is double maxWidth &&
            values[2] is double percentage)
        {
            if (percentage >= 100.0)
            {
                // respect line break
                return actualWidth;
            }

            var calcWidth = actualWidth * (percentage / 100.0);

            if (maxWidth > 0)
            {
                return Math.Min(maxWidth, calcWidth);
            }

            return calcWidth;
        }
        return 0;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class SubTextMaskConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length != 6)
            return DependencyProperty.UnsetValue;

        if (values[0] is int index &&          // Index of sub to be processed
            values[1] is string displayText && // Sub text to be processed (may be translated)
            values[2] is string text &&        // Sub text to be processed
            values[3] is int selectedIndex &&  // Index of the selected ListItem
            values[4] is bool isEnabled &&     // whether to enable this feature
            values[5] is bool showOriginal)    // whether to show original text
        {
            string show = showOriginal ? text : displayText;

            if (isEnabled && index > selectedIndex && !string.IsNullOrEmpty(show))
            {
                return new string('_', show.Length);
            }

            return show;
        }
        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

public class SubTextFlowDirectionConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length != 3)
        {
            return DependencyProperty.UnsetValue;
        }

        if (values[0] is bool isTranslated &&
            values[1] is int subIndex &&
            values[2] is FlyleafManager fl)
        {
            var language = isTranslated ? fl.PlayerConfig.Subtitles.TranslateLanguage : fl.Player.SubtitlesManager[subIndex].Language;
            if (language != null && language.IsRTL)
            {
                return FlowDirection.RightToLeft;
            }

            return FlowDirection.LeftToRight;
        }

        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

public class SubIsPlayingConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length != 3)
        {
            return DependencyProperty.UnsetValue;
        }

        if (values[0] is int index &&
            values[1] is int currentIndex &&
            values[2] is bool isDisplaying)
        {
            return isDisplaying && index == currentIndex;
        }

        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

public class ListItemIndexVisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length != 3)
        {
            return DependencyProperty.UnsetValue;
        }

        if (values[0] is int itemIndex &&     // Index of sub to be processed
            values[1] is int selectedIndex && // Index of the selected ListItem
            values[2] is bool isEnabled)      // whether to enable this feature
        {
            if (isEnabled && itemIndex > selectedIndex)
            {
                return Visibility.Collapsed;
            }

            return Visibility.Visible;
        }
        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

[ValueConversion(typeof(TimeSpan), typeof(string))]
public class TimeSpanToStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is TimeSpan ts)
        {
            if (ts.TotalHours >= 1)
            {
                return ts.ToString(@"hh\:mm\:ss");
            }
            else
            {
                return ts.ToString(@"mm\:ss");
            }
        }

        return DependencyProperty.UnsetValue;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(SubtitleData), typeof(WriteableBitmap))]
public class SubBitmapImageSourceConverter : IValueConverter
{
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is not SubtitleData item)
        {
            return null;
        }

        if (!item.IsBitmap || item.Bitmap == null || !string.IsNullOrEmpty(item.Text))
        {
            return null;
        }

        WriteableBitmap wb = item.Bitmap.SubToWritableBitmap(false);

        return wb;
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
        => throw new NotImplementedException();
}

[ValueConversion(typeof(int), typeof(string))]
public class SubIndexToDisplayStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is int subIndex)
        {
            return subIndex == 0 ? "Primary" : "Secondary";
        }
        return DependencyProperty.UnsetValue;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string str)
        {
            return str == "Primary" ? 0 : 1;
        }
        return DependencyProperty.UnsetValue;
    }
}

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

namespace FlyleafLib.MediaPlayer.Translation.Services;

#nullable enable

// All LLM translation use this class
// Currently only supports OpenAI compatible API
public class OpenAIBaseTranslateService : ITranslateService
{
    private readonly HttpClient _httpClient;
    private readonly OpenAIBaseTranslateSettings _settings;
    private readonly TranslateChatConfig _chatConfig;
    private readonly bool _wordMode;

    private ChatTranslateMethod TranslateMethod => _chatConfig.TranslateMethod;

    public OpenAIBaseTranslateService(OpenAIBaseTranslateSettings settings, TranslateChatConfig chatConfig, bool wordMode)
    {
        _httpClient = settings.GetHttpClient();
        _settings = settings;
        _chatConfig = chatConfig;
        _wordMode = wordMode;
    }

    private string? _basePrompt;
    private readonly ConcurrentQueue<OpenAIMessage> _messageQueue = new();

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    };

    public TranslateServiceType ServiceType => _settings.ServiceType;

    public void Dispose()
    {
        _httpClient.Dispose();
    }

    public void Initialize(Language src, TargetLanguage target)
    {
        (TranslateLanguage srcLang, TranslateLanguage targetLang) = this.TryGetLanguage(src, target);

        // setup prompt
        string prompt = !_wordMode && TranslateMethod == ChatTranslateMethod.KeepContext
            ? _chatConfig.PromptKeepContext
            : _chatConfig.PromptOneByOne;

        string targetLangName = _chatConfig.IncludeTargetLangRegion
            ? target.DisplayName() : targetLang.Name;

        _basePrompt = prompt
            .Replace("{source_lang}", srcLang.Name)
            .Replace("{target_lang}", targetLangName);
    }

    public async Task<string> TranslateAsync(string text, CancellationToken token)
    {
        if (!_wordMode && TranslateMethod == ChatTranslateMethod.KeepContext)
        {
            return await DoKeepContext(text, token);
        }

        return await DoOneByOne(text, token);
    }

    private async Task<string> DoKeepContext(string text, CancellationToken token)
    {
        if (_basePrompt == null)
            throw new InvalidOperationException("must be initialized");

        // Trim message history if required
        while (_messageQueue.Count / 2 > _chatConfig.SubtitleContextCount)
        {
            if (_chatConfig.ContextRetainPolicy == ChatContextRetainPolicy.KeepSize)
            {
                Debug.Assert(_messageQueue.Count >= 2);

                // user
                _messageQueue.TryDequeue(out _);
                // assistant
                _messageQueue.TryDequeue(out _);
            }
            else if (_chatConfig.ContextRetainPolicy == ChatContextRetainPolicy.Reset)
            {
                // clear
                _messageQueue.Clear();
            }
        }

        List<OpenAIMessage> messages = new(_messageQueue.Count + 2)
        {
            new OpenAIMessage { role = "system", content = _basePrompt },
        };

        // add history
        messages.AddRange(_messageQueue);

        // add new message
        OpenAIMessage newMessage = new() { role = "user", content = text };
        messages.Add(newMessage);

        string reply = await SendChatRequest(
            _httpClient, _settings, messages.ToArray(), token);

        // add to message history if success
        _messageQueue.Enqueue(newMessage);
        _messageQueue.Enqueue(new OpenAIMessage { role = "assistant", content = reply });

        return reply;
    }

    private async Task<string> DoOneByOne(string text, CancellationToken token)
    {
        if (_basePrompt == null)
            throw new InvalidOperationException("must be initialized");

        string prompt = _basePrompt.Replace("{source_text}", text);

        OpenAIMessage[] messages =
        [
            new() { role = "user", content = prompt }
        ];

        return await SendChatRequest(_httpClient, _settings, messages, token);
    }

    public static async Task<string> Hello(OpenAIBaseTranslateSettings settings)
    {
        using HttpClient client = settings.GetHttpClient();

        OpenAIMessage[] messages =
        [
            new() { role = "user", content = "Hello" }
        ];

        return await SendChatRequest(client, settings, messages, CancellationToken.None);
    }

    private static async Task<string> SendChatRequest(
        HttpClient client,
        OpenAIBaseTranslateSettings settings,
        OpenAIMessage[] messages,
        CancellationToken token)
    {
        string jsonResultString = string.Empty;
        int statusCode = -1;

        // Create the request payload
        OpenAIRequest request = new()
        {
            model = settings.Model,
            stream = false,
            messages = messages,

            temperature = settings.TemperatureManual ? settings.Temperature : null,
            top_p = settings.TopPManual ? settings.TopP : null,
            max_completion_tokens = settings.MaxCompletionTokens,
            max_tokens = settings.MaxTokens,
        };

        if (!settings.ModelRequired && string.IsNullOrWhiteSpace(settings.Model))
        {
            request.model = null;
        }

        try
        {
            // Convert to JSON
            string jsonContent = JsonSerializer.Serialize(request, JsonOptions);
            using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
            using var result = await client.PostAsync(settings.ChatPath, content, token);

            jsonResultString = await result.Content.ReadAsStringAsync(token);

            statusCode = (int)result.StatusCode;
            result.EnsureSuccessStatusCode();

            OpenAIResponse? chatResponse = JsonSerializer.Deserialize<OpenAIResponse>(jsonResultString);
            string reply = chatResponse!.choices[0].message.content;
            if (settings.ReasonStripRequired)
            {
                var stripped = ChatReplyParser.StripReasoning(reply);
                return stripped.Trim().ToString();
            }

            return reply.Trim();
        }
        // Distinguish between timeout and cancel errors
        catch (OperationCanceledException ex)
            when (!ex.Message.StartsWith("The request was canceled due to the configured HttpClient.Timeout"))
        {
            // cancel
            throw;
        }
        catch (Exception ex)
        {
            // timeout and other error
            throw new TranslationException($"Cannot request to {settings.ServiceType}: {ex.Message}", ex)
            {
                Data =
                {
                    ["status_code"] = statusCode.ToString(),
                    ["response"] = jsonResultString
                }
            };
        }
    }

    public static async Task<List<string>> GetLoadedModels(OpenAIBaseTranslateSettings settings)
    {
        using HttpClient client = settings.GetHttpClient(true);

        string jsonResultString = string.Empty;
        int statusCode = -1;

        // getting models
        try
        {
            using var result = await client.GetAsync("/v1/models");

            jsonResultString = await result.Content.ReadAsStringAsync();

            statusCode = (int)result.StatusCode;
            result.EnsureSuccessStatusCode();

            JsonNode? node = JsonNode.Parse(jsonResultString);
            List<string> models = node!["data"]!.AsArray()
                .Select(model => model!["id"]!.GetValue<string>())
                .Order()
                .ToList();

            return models;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"get models error: {ex.Message}", ex)
            {
                Data =
                {
                    ["status_code"] = statusCode.ToString(),
                    ["response"] = jsonResultString
                }
            };
        }
    }
}

public class OpenAIMessage
{
    public required string role { get; init; }
    public required string content { get; init; }
}

public class OpenAIRequest
{
    public string? model { get; set; }
    public required OpenAIMessage[] messages { get; init; }
    public required bool stream { get; init; }
    public double? temperature { get; set; }
    public double? top_p { get; set; }
    public int? max_completion_tokens { get; set; }
    public int? max_tokens { get; set; }
}

public class OpenAIResponse
{
    public required OpenAIChoice[] choices { get; init; }
}

public class OpenAIChoice
{
    public required OpenAIMessage message { get; init; }
}

public static class ChatReplyParser
{
    // Target tag names to remove (lowercase)
    private static readonly string[] Tags = ["think", "reason", "reasoning", "thought"];

    // open/close tag strings from tag names
    private static readonly string[] OpenTags;
    private static readonly string[] CloseTags;

    static ChatReplyParser()
    {
        OpenTags = new string[Tags.Length];
        CloseTags = new string[Tags.Length];
        for (int i = 0; i < Tags.Length; i++)
        {
            OpenTags[i] = $"<{Tags[i]}>";       // e.g. "<think>"
            CloseTags[i] = $"</{Tags[i]}>";    // e.g. "</think>"
        }
    }

    /// <summary>
    /// Removes a leading reasoning tag if present and returns only the generated message portion.
    /// </summary>
    public static ReadOnlySpan<char> StripReasoning(ReadOnlySpan<char> input)
    {
        // Return immediately if it doesn't start with a tag
        if (input.Length == 0 || input[0] != '<')
        {
            return input;
        }

        for (int i = 0; i < OpenTags.Length; i++)
        {
            if (input.StartsWith(OpenTags[i], StringComparison.OrdinalIgnoreCase))
            {
                int endIdx = input.IndexOf(CloseTags[i], StringComparison.OrdinalIgnoreCase);
                if (endIdx >= 0)
                {
                    int next = endIdx + CloseTags[i].Length;
                    // Skip over any consecutive line breaks and whitespace
                    while (next < input.Length && char.IsWhiteSpace(input[next]))
                    {
                        next++;
                    }
                    return input.Slice(next);
                }
            }
        }

        // Return original string if no tag matched
        return input;
    }
}

using System.Collections.Concurrent;
using System.Threading;

using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRemuxer;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDecoder;

/* TODO
 *
 * Circular Buffer
 * - Safe re-allocation and check also actual frames in queue (as now during draining we can overwrite them)
 * - Locking with Audio.AddSamples during re-allocation and re-write old data to the new buffer and update the pointers in queue
 *
 * Filters
 * - Note: Performance issue (for seek/speed change). We can't drain the buffersrc and re-use the filtergraph without re-initializing it, is not supported
 * - Check if av_buffersrc_get_nb_failed_requests required
 * - Add Config for filter threads?
 * - Review Access Violation issue with dynaudnorm/loudnorm filters in combination with atempo (when changing speed to fast?)
 * - Use multiple atempo for better quality (for < 0.5 and > 2, use eg. 2 of sqrt(X) * sqrt(X) to achive this)
 * - Review locks / recode RunInternal to be able to continue from where it stopped (eg. ProcessFilter)
 *
 * Custom Frames Queue to notify when the queue is not full anymore (to avoid thread sleep which can cause delays)
 * Support more output formats/channels/sampleRates (and currently output to 32-bit, sample rate to 48Khz and not the same as input? - should calculate possible delays)
 */

public unsafe partial class AudioDecoder : DecoderBase
{
    public AudioStream      AudioStream         => (AudioStream) Stream;
    public readonly
            VideoDecoder    VideoDecoder;
    public ConcurrentQueue<AudioFrame>
                            Frames              { get; protected set; } = new();

    static AVSampleFormat   AOutSampleFormat    = AVSampleFormat.S16;
    static string           AOutSampleFormatStr = av_get_sample_fmt_name(AOutSampleFormat);
    static AVChannelLayout  AOutChannelLayout   = AV_CHANNEL_LAYOUT_STEREO;// new() { order = AVChannelOrder.Native, nb_channels = 2, u = new AVChannelLayout_u() { mask = AVChannel.for AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT} };
    static int              AOutChannels        = AOutChannelLayout.nb_channels;
    static int              ASampleBytes        = av_get_bytes_per_sample(AOutSampleFormat) * AOutChannels;

    public readonly object  CircularBufferLocker= new();
    internal Action         CBufAlloc;          // Informs Audio player to clear buffer pointers to avoid access violation
    static int              cBufTimesSize       = 4;
    int                     cBufTimesCur        = 1;
    byte[]                  cBuf;
    int                     cBufPos;
    int                     cBufSamples;
    internal bool           resyncWithVideoRequired;
    SwrContext*             swrCtx;

    internal long           nextPts;
    double                  sampleRateTimebase;

    public AudioDecoder(Config config, int uniqueId = -1, VideoDecoder syncDecoder = null) : base(config, uniqueId)
        => VideoDecoder = syncDecoder;

    protected override int Setup(AVCodec* codec) => 0;
    private int SetupSwr()
    {
        int ret;

        DisposeSwr();
        swrCtx = swr_alloc();

        av_opt_set_chlayout(swrCtx,     "in_chlayout",          &codecCtx->ch_layout,   0);
        av_opt_set_int(swrCtx,          "in_sample_rate",       codecCtx->sample_rate,  0);
        av_opt_set_sample_fmt(swrCtx,   "in_sample_fmt",        codecCtx->sample_fmt,   0);

        fixed(AVChannelLayout* ptr = &AOutChannelLayout)
        av_opt_set_chlayout(swrCtx,     "out_chlayout",         ptr, 0);
        av_opt_set_int(swrCtx,          "out_sample_rate",      codecCtx->sample_rate,  0);
        av_opt_set_sample_fmt(swrCtx,   "out_sample_fmt",       AOutSampleFormat,       0);

        ret = swr_init(swrCtx);
        if (ret < 0)
            Log.Error($"Swr setup failed {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

        return ret;
    }
    private void DisposeSwr()
    {
        if (swrCtx == null)
            return;

        swr_close(swrCtx);

        fixed(SwrContext** ptr = &swrCtx)
            swr_free(ptr);

        swrCtx = null;
    }

    protected override void DisposeInternal()
    {
        DisposeFrames();
        DisposeSwr();
        DisposeFilters();

        lock (CircularBufferLocker)
        cBuf            = null;
        cBufSamples     = 0;
        filledFromCodec = false;
        nextPts         = AV_NOPTS_VALUE;
    }
    public void DisposeFrames() => Frames = new();
    public void Flush()
    {
        lock (lockActions)
            lock (lockCodecCtx)
            {
                if (Disposed)
                    return;

                if (Status == Status.Ended)
                    Status = Status.Stopped;
                else if (Status == Status.Draining)
                    Status = Status.Stopping;

                resyncWithVideoRequired = !VideoDecoder.Disposed;
                nextPts = AV_NOPTS_VALUE;
                DisposeFrames();
                avcodec_flush_buffers(codecCtx);
                if (filterGraph != null)
                    SetupFilters();
            }
    }

    protected override void RunInternal()
    {
        int ret = 0;
        int allowedErrors = Config.Decoder.MaxErrors;
        int sleepMs = Config.Decoder.MaxAudioFrames > 5 && Config.Player.MaxLatency == 0 ? 10 : 4;
        AVPacket *packet;

        do
        {
            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxAudioFrames)
            {
                lock (lockStatus)
                    if (Status == Status.Running)
                        Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxAudioFrames && Status == Status.QueueFull)
                    Thread.Sleep(sleepMs);

                lock (lockStatus)
                {
                    if (Status != Status.QueueFull)
                        break;

                    Status = Status.Running;
                }
            }

            // While Packets Queue Empty (Ended | Quit if Demuxer stopped | Wait until we get packets)
            if (demuxer.AudioPackets.Count == 0)
            {
                CriticalArea = true;

                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueEmpty;

                while (demuxer.AudioPackets.Count == 0 && Status == Status.QueueEmpty)
                {
                    if (demuxer.Status == Status.Ended)
                    {
                        lock (lockStatus)
                        {
                            // TODO: let the demuxer push the draining packet
                            Log.Debug("Draining");
                            Status = Status.Draining;
                            var drainPacket = av_packet_alloc();
                            drainPacket->data = null;
                            drainPacket->size = 0;
                            demuxer.AudioPackets.Enqueue(drainPacket);
                        }

                        break;
                    }
                    else if (!demuxer.IsRunning)
                    {
                        if (CanDebug) Log.Debug($"Demuxer is not running [Demuxer Status: {demuxer.Status}]");

                        int retries = 5;

                        while (retries > 0)
                        {
                            retries--;
                            Thread.Sleep(10);
                            if (demuxer.IsRunning) break;
                        }

                        lock (demuxer.lockStatus)
                        lock (lockStatus)
                        {
                            if (demuxer.Status == Status.Pausing || demuxer.Status == Status.Paused)
                                Status = Status.Pausing;
                            else if (demuxer.Status != Status.Ended)
                                Status = Status.Stopping;
                            else
                                continue;
                        }

                        break;
                    }

                    Thread.Sleep(sleepMs);
                }

                lock (lockStatus)
                {
                    CriticalArea = false;
                    if (Status != Status.QueueEmpty && Status != Status.Draining) break;
                    if (Status != Status.Draining) Status = Status.Running;
                }
            }

            Monitor.Enter(lockCodecCtx); // restore the old lock / add interrupters similar to the demuxer
            try
            {
                if (Status == Status.Stopped)
                    { Monitor.Exit(lockCodecCtx); continue; }

                packet = demuxer.AudioPackets.Dequeue();

                if (packet == null)
                    { Monitor.Exit(lockCodecCtx); continue; }

                if (isRecording)
                {
                    if (!recGotKeyframe && VideoDecoder.StartRecordTime != AV_NOPTS_VALUE && (long)(packet->pts * AudioStream.Timebase) - demuxer.StartTime > VideoDecoder.StartRecordTime)
                        recGotKeyframe = true;

                    if (recGotKeyframe)
                        curRecorder.Write(av_packet_clone(packet), !OnVideoDemuxer);
                }

                ret = avcodec_send_packet(codecCtx, packet);
                av_packet_free(&packet);

                if (ret != 0 && ret != AVERROR(EAGAIN))
                {
                    if (ret == AVERROR_EOF)
                    {
                        Status = Status.Ended;
                        break;
                    }
                    else
                    {
                        allowedErrors--;
                        if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                        if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                        Monitor.Exit(lockCodecCtx); continue;
                    }
                }

                while (true)
                {
                    ret = avcodec_receive_frame(codecCtx, frame);
                    if (ret != 0)
                    {
                        av_frame_unref(frame);

                        if (ret == AVERROR_EOF && filterGraph != null)
                        {
                            lock (lockSpeed)
                            {
                                DrainFilters();
                                Status = Status.Ended;
                            }
                        }

                        break;
                    }

                    if (frame->best_effort_timestamp != AV_NOPTS_VALUE)
                        frame->pts = frame->best_effort_timestamp;
                    else if (frame->pts == AV_NOPTS_VALUE)
                    {
                        if (nextPts == AV_NOPTS_VALUE && filledFromCodec) // Possible after seek (maybe set based on pkt_pos?)
                        {
                            av_frame_unref(frame);
                            continue;
                        }

                        frame->pts = nextPts;
                    }

                    // We could fix it down to the demuxer based on size?
                    if (frame->duration <= 0)
                        frame->duration = av_rescale_q((long)(frame->nb_samples * sampleRateTimebase), Engine.FFmpeg.AV_TIMEBASE_Q, Stream.AVStream->time_base);

                    bool codecChanged = AudioStream.SampleFormat != codecCtx->sample_fmt || AudioStream.SampleRate != codecCtx->sample_rate || AudioStream.ChannelLayout != codecCtx->ch_layout.u.mask;

                    if (!filledFromCodec || codecChanged)
                    {
                        if (codecChanged && filledFromCodec)
                        {
                            byte[] buf = new byte[50];
                            fixed (byte* bufPtr = buf)
                            {
                                av_channel_layout_describe(&codecCtx->ch_layout, bufPtr, (nuint)buf.Length);
                                Log.Warn($"Codec changed {AudioStream.CodecIDOrig} {AudioStream.SampleFormat} {AudioStream.SampleRate} {AudioStream.ChannelLayoutStr} => {codecCtx->codec_id} {codecCtx->sample_fmt} {codecCtx->sample_rate} {Utils.BytePtrToStringUTF8(bufPtr)}");
                            }
                        }

                        DisposeInternal();
                        filledFromCodec = true;

                        avcodec_parameters_from_context(Stream.AVStream->codecpar, codecCtx);
                        AudioStream.AVStream->time_base = codecCtx->pkt_timebase;
                        AudioStream.Refresh();
                        resyncWithVideoRequired = !VideoDecoder.Disposed;
                        sampleRateTimebase = 1000 * 1000.0 / codecCtx->sample_rate;
                        nextPts = AudioStream.StartTimePts;

                        if (frame->pts == AV_NOPTS_VALUE)
                            frame->pts = nextPts;

                        ret = SetupFiltersOrSwr();

                        CodecChanged?.Invoke(this);

                        if (ret != 0)
                        {
                            Status = Status.Stopping;
                            av_frame_unref(frame);
                            break;
                        }

                        if (nextPts == AV_NOPTS_VALUE)
                        {
                            av_frame_unref(frame);
                            continue;
                        }
                    }

                    if (resyncWithVideoRequired)
                    {
                        // TODO: in case of long distance will spin (CPU issue), possible reseek?
                        while (VideoDecoder.StartTime == AV_NOPTS_VALUE && VideoDecoder.IsRunning && resyncWithVideoRequired)
                            Thread.Sleep(10);

                        long ts = (long)((frame->pts + frame->duration) * AudioStream.Timebase) - demuxer.StartTime + Config.Audio.Delay;

                        if (ts < VideoDecoder.StartTime)
                        {
                            if (CanTrace) Log.Trace($"Drops {Utils.TicksToTime(ts)} (< V: {Utils.TicksToTime(VideoDecoder.StartTime)})");
                            av_frame_unref(frame);
                            continue;
                        }
                        else
                            resyncWithVideoRequired = false;
                    }

                    lock (lockSpeed)
                    {
                        if (filterGraph != null)
                            ProcessFilters();
                        else
                            Process();

                        av_frame_unref(frame);
                    }
                }
            } catch { }

            Monitor.Exit(lockCodecCtx);

        } while (Status == Status.Running);

        if (isRecording) { StopRecording(); recCompleted(MediaType.Audio); }

        if (Status == Status.Draining) Status = Status.Ended;
    }
    private void Process()
    {
        try
        {
            nextPts = frame->pts + frame->duration;

            var dataLen     = frame->nb_samples * ASampleBytes;
            var speedDataLen= Utils.Align((int)(dataLen / speed), ASampleBytes);

            AudioFrame mFrame = new()
            {
                timestamp   = (long)(frame->pts * AudioStream.Timebase) - demuxer.StartTime + Config.Audio.Delay,
                dataLen     = speedDataLen
            };
            if (CanTrace) Log.Trace($"Processes {Utils.TicksToTime(mFrame.timestamp)}");

            if (frame->nb_samples > cBufSamples)
                AllocateCircularBuffer(frame->nb_samples);
            else if (cBufPos + Math.Max(dataLen, speedDataLen) >= cBuf.Length)
                cBufPos     = 0;

            fixed (byte *circularBufferPosPtr = &cBuf[cBufPos])
            {
                int ret = swr_convert(swrCtx, &circularBufferPosPtr, frame->nb_samples, (byte**)&frame->data, frame->nb_samples);
                if (ret < 0)
                    return;

                mFrame.dataPtr = (IntPtr)circularBufferPosPtr;
            }

            // Fill silence
            if (speed < 1)
                for (int p = dataLen; p < speedDataLen; p++)
                    cBuf[cBufPos + p] = 0;

            cBufPos += Math.Max(dataLen, speedDataLen);
            Frames.Enqueue(mFrame);

            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxAudioFrames * cBufTimesCur)
            {
                Monitor.Exit(lockCodecCtx);
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxAudioFrames * cBufTimesCur && Status == Status.QueueFull)
                    Thread.Sleep(20);

                Monitor.Enter(lockCodecCtx);

                lock (lockStatus)
                {
                    if (Status != Status.QueueFull)
                        return;

                    Status = Status.Running;
                }
            }
        }
        catch (Exception e)
        {
            Log.Error($"Failed to process frame ({e.Message})");
        }
    }

    private void AllocateCircularBuffer(int samples)
    {
        /* TBR
        * 1. If we change to different in/out sample rates we need to calculate delay
        * 2. By destorying the cBuf can create critical issues while the audio decoder reads the data? (add lock) | we need to copy the lost data and change the pointers
        * 3. Recalculate on Config.Decoder.MaxAudioFrames change (greater)
        * 4. cBufTimesSize cause filters can pass the limit when we need to use lockSpeed
        */

        samples = Math.Max(10000, samples); // 10K samples to ensure that currently we will not re-allocate?
        int size    = Config.Decoder.MaxAudioFrames * samples * ASampleBytes * cBufTimesSize;
        Log.Debug($"Re-allocating circular buffer ({samples} > {cBufSamples}) with {size}bytes");

        lock (CircularBufferLocker)
        {
            DisposeFrames(); // TODO: copy data
            CBufAlloc?.Invoke();
            cBuf        = new byte[size];
            cBufPos     = 0;
            cBufSamples = samples;
        }

    }

    #region Recording
    internal Action<MediaType>
            recCompleted;
    Remuxer curRecorder;
    bool    recGotKeyframe;
    internal bool isRecording;
    internal void StartRecording(Remuxer remuxer)
    {
        if (Disposed || isRecording) return;

        curRecorder     = remuxer;
        isRecording     = true;
        recGotKeyframe  = VideoDecoder.Disposed || VideoDecoder.Stream == null;
    }
    internal void StopRecording() => isRecording = false;
    #endregion
}

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FlyleafLib.MediaFramework.MediaStream;
using Lingua;

namespace FlyleafLib.Plugins;

public class OpenSubtitles : PluginBase, IOpenSubtitles, ISearchLocalSubtitles
{
    public new int Priority { get; set; } = 3000;

    private static readonly Lazy<LanguageDetector> LanguageDetector = new(() =>
    {
        LanguageDetector detector = LanguageDetectorBuilder
            .FromAllLanguages()
            .Build();

        return detector;
    }, true);

    private static readonly HashSet<string> ExtSet = new(Utils.ExtensionsSubtitles, StringComparer.OrdinalIgnoreCase);

    public OpenSubtitlesResults Open(string url)
    {
        foreach (var extStream in Selected.ExternalSubtitlesStreamsAll)
            if (extStream.Url == url)
                return new(extStream);

        string title;

        if (File.Exists(url))
        {
            Selected.FillMediaParts();

            FileInfo fi = new(url);
            title       = fi.Name;
        }
        else
        {
            try
            {
                Uri uri = new(url);
                title = Path.GetFileName(uri.LocalPath);

                if (title == null || title.Trim().Length == 0)
                    title = url;

            } catch
            {
                title = url;
            }
        }

        ExternalSubtitlesStream newExtStream = new()
        {
            Url         = url,
            Title       = title,
            Downloaded  = true,
            IsBitmap    = IsSubtitleBitmap(url),
        };

        if (Config.Subtitles.LanguageAutoDetect && !newExtStream.IsBitmap)
        {
            newExtStream.Language = DetectLanguage(url);
            newExtStream.LanguageDetected = true;
        }

        AddExternalStream(newExtStream);

        return new(newExtStream);
    }

    public OpenSubtitlesResults Open(Stream iostream) => null;

    public void SearchLocalSubtitles()
    {
        try
        {
            string mediaDir = Path.GetDirectoryName(Playlist.Url);
            string mediaName = Path.GetFileNameWithoutExtension(Playlist.Url);

            OrderedDictionary<string, Language> result = new(StringComparer.OrdinalIgnoreCase);

            CollectFromDirectory(mediaDir, mediaName, result);

            // also search in subdirectories
            string paths = Config.Subtitles.SearchLocalPaths;
            if (!string.IsNullOrWhiteSpace(paths))
            {
                foreach (Range seg in paths.AsSpan().Split(';'))
                {
                    var path = paths.AsSpan(seg).Trim();
                    if (path.IsEmpty) continue;

                    string searchDir = !Path.IsPathRooted(path)
                        ? Path.Join(mediaDir, path)
                        : path.ToString();

                    if (Directory.Exists(searchDir))
                    {
                        CollectFromDirectory(searchDir, mediaName, result);
                    }
                }
            }

            if (result.Count == 0)
            {
                return;
            }

            Selected.FillMediaParts();

            foreach (var (path, lang) in result)
            {
                if (Selected.ExternalSubtitlesStreamsAll.Any(s => s.Url == path))
                {
                    continue;
                }

                FileInfo fi = new(path);
                string title = fi.Name;

                ExternalSubtitlesStream sub = new()
                {
                    Url = path,
                    Title = title,
                    Downloaded = true,
                    IsBitmap = IsSubtitleBitmap(path),
                    Language = lang
                };

                if (Config.Subtitles.LanguageAutoDetect && !sub.IsBitmap && lang == Language.Unknown)
                {
                    sub.Language = DetectLanguage(path);
                    sub.LanguageDetected = true;
                }

                Log.Debug($"Adding [{sub.Language.TopEnglishName}] {path}");

                AddExternalStream(sub);
            }
        }
        catch (Exception e)
        {
            Log.Error($"SearchLocalSubtitles failed ({e.Message})");
        }
    }

    private static void CollectFromDirectory(string searchDir, string filename, IDictionary<string, Language> result)
    {
        HashSet<int> added = null;

        // Get files starting with the same filename
        List<string> fileList;
        try
        {
            fileList = Directory.GetFiles(searchDir, $"{filename}.*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive })
                .ToList();
        }
        catch
        {
            return;
        }

        if (fileList.Count == 0)
        {
            return;
        }

        var files = fileList.Select(f => new
        {
            FullPath = f,
            FileName = Path.GetFileName(f)
        }).ToList();

        // full match with top priority (video.srt, video.ass)
        foreach (string ext in ExtSet)
        {
            string expect = $"{filename}.{ext}";
            int match = files.FindIndex(x => string.Equals(x.FileName, expect, StringComparison.OrdinalIgnoreCase));
            if (match != -1)
            {
                result.TryAdd(files[match].FullPath, Language.Unknown);
                added ??= new HashSet<int>();
                added.Add(match);
            }
        }

        // head match (video.*.srt, video.*.ass)
        var extSetLookup = ExtSet.GetAlternateLookup<ReadOnlySpan<char>>();
        foreach (var (i, x) in files.Index())
        {
            // skip full match
            if (added != null && added.Contains(i))
            {
                continue;
            }

            var span = x.FileName.AsSpan();
            var fileExt = Path.GetExtension(span).TrimStart('.');

            // Check if the file is a subtitle file by its extension
            if (extSetLookup.Contains(fileExt))
            {
                var name = Path.GetFileNameWithoutExtension(span);

                if (!name.StartsWith(filename + '.', StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                Language lang = Language.Unknown;

                var extraPart = name.Slice(filename.Length + 1); // Skip file name and dot
                if (extraPart.Length > 0)
                {
                    foreach (var codeSeg in extraPart.Split('.'))
                    {
                        var code = extraPart[codeSeg];
                        if (code.Length > 0)
                        {
                            Language parsed = Language.Get(code.ToString());
                            if (!string.IsNullOrEmpty(parsed.IdSubLanguage) && parsed.IdSubLanguage != "und")
                            {
                                lang = parsed;
                                break;
                            }
                        }
                    }
                }

                result.TryAdd(x.FullPath, lang);
            }
        }
    }

    // TODO: L: To check the contents of a file by determining the bitmap.
    private static bool IsSubtitleBitmap(string path)
    {
        try
        {
            FileInfo fi = new(path);

            return Utils.ExtensionsSubtitlesBitmap.Contains(fi.Extension.TrimStart('.').ToLower());
        }
        catch
        {
            return false;
        }
    }

    // TODO: L: Would it be better to check with SubtitlesManager for network subtitles?
    private static Language DetectLanguage(string path)
    {
        if (!File.Exists(path))
        {
            return Language.Unknown;
        }

        Encoding encoding = Encoding.Default;
        Encoding detected = TextEncodings.DetectEncoding(path);

        if (detected != null)
        {
            encoding = detected;
        }

        // TODO: L: refactor: use the code for reading subtitles in Demuxer
        byte[] data = new byte[100 * 1024];

        try
        {
            using FileStream fs = new(path, FileMode.Open, FileAccess.Read);
            int bytesRead = fs.Read(data, 0, data.Length);
            Array.Resize(ref data, bytesRead);
        }
        catch
        {
            return Language.Unknown;
        }

        string content = encoding.GetString(data);

        var detectedLanguage = LanguageDetector.Value.DetectLanguageOf(content);

        if (detectedLanguage == Lingua.Language.Unknown)
        {
            return Language.Unknown;
        }

        return Language.Get(detectedLanguage.IsoCode6391().ToString().ToLower());
    }
}

using System.Windows;
using System.Windows.Controls;
using FlyleafLib;
using FlyleafLib.MediaPlayer.Translation;
using LLPlayer.Extensions;
using LLPlayer.Services;
using LLPlayer.Views;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitles : UserControl
{
    public SettingsSubtitles()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsSubtitlesVM>();
    }
}

public class SettingsSubtitlesVM : Bindable
{
    public FlyleafManager FL { get; }
    private readonly IDialogService _dialogService;

    public SettingsSubtitlesVM(FlyleafManager fl, IDialogService dialogService)
    {
        _dialogService = dialogService;
        FL = fl;
        Languages = TranslateLanguage.Langs.Values.ToList();

        if (FL.PlayerConfig.Subtitles.LanguageFallbackPrimary != null)
        {
            SelectedPrimaryLanguage = Languages.FirstOrDefault(l => l.ISO6391 == FL.PlayerConfig.Subtitles.LanguageFallbackPrimary.ISO6391);
        }

        if (FL.PlayerConfig.Subtitles.LanguageFallbackSecondary != null)
        {
            SelectedSecondaryLanguage = Languages.FirstOrDefault(l => l.ISO6391 == FL.PlayerConfig.Subtitles.LanguageFallbackSecondary.ISO6391);
        }
    }

    public List<TranslateLanguage> Languages { get; set; }

    public TranslateLanguage? SelectedPrimaryLanguage
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (value == null)
                {
                    FL.PlayerConfig.Subtitles.LanguageFallbackPrimary = null;
                }
                else
                {
                    FL.PlayerConfig.Subtitles.LanguageFallbackPrimary = Language.Get(value.ISO6391);
                }

                if (FL.PlayerConfig.Subtitles.LanguageFallbackSecondarySame)
                {
                    FL.PlayerConfig.Subtitles.LanguageFallbackSecondary = FL.PlayerConfig.Subtitles.LanguageFallbackPrimary;

                    SelectedSecondaryLanguage = SelectedPrimaryLanguage;
                }
            }
        }
    }

    public TranslateLanguage? SelectedSecondaryLanguage
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                if (value == null)
                {
                    FL.PlayerConfig.Subtitles.LanguageFallbackSecondary = null;
                }
                else
                {
                    FL.PlayerConfig.Subtitles.LanguageFallbackSecondary = Language.Get(value.ISO6391);
                }
            }
        }
    }

    public DelegateCommand? CmdConfigureLanguage => field ??= new(() =>
    {
        DialogParameters p = new()
        {
            { "languages", FL.PlayerConfig.Subtitles.Languages }
        };

        _dialogService.ShowDialog(nameof(SelectLanguageDialog), p, result =>
        {
            List<Language> updated = result.Parameters.GetValue<List<Language>>("languages");

            if (!FL.PlayerConfig.Subtitles.Languages.SequenceEqual(updated))
            {
                FL.PlayerConfig.Subtitles.Languages = updated;
            }
        });
    });
}

using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class CheatSheetDialog : UserControl
{
    public CheatSheetDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<CheatSheetDialogVM>();
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        Window? window = Window.GetWindow(this);
        window!.CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, OnFindExecuted));
    }

    private void OnFindExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        SearchBox.Focus();
        SearchBox.SelectAll();
        e.Handled = true;
    }
}

[ValueConversion(typeof(int), typeof(Visibility))]
public class CountToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (int.TryParse(value.ToString(), out var count))
        {
            return count == 0 ? Visibility.Collapsed : Visibility.Visible;
        }

        return Visibility.Visible;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

namespace FlyleafLib;

public class FFmpegEngine
{
    public string   Folder          { get; private set; }
    public string   Version         { get; private set; }

    const int           AV_LOG_BUFFER_SIZE = 5 * 1024;
    internal AVRational AV_TIMEBASE_Q;

    internal FFmpegEngine()
    {
        try
        {
            Engine.Log.Info($"Loading FFmpeg libraries from '{Engine.Config.FFmpegPath}'");
            Folder = Utils.GetFolderPath(Engine.Config.FFmpegPath);
            LoadLibraries(Folder, Engine.Config.FFmpegLoadProfile);

            uint ver = avformat_version();
            Version = $"{ver >> 16}.{(ver >> 8) & 255}.{ver & 255}";

            SetLogLevel();
            AV_TIMEBASE_Q   = av_get_time_base_q();
            Engine.Log.Info($"FFmpeg Loaded (Profile: {Engine.Config.FFmpegLoadProfile}, Location: {Folder}, FmtVer: {Version})");
        } catch (Exception e)
        {
            Engine.Log.Error($"Loading FFmpeg libraries '{Engine.Config.FFmpegPath}' failed\r\n{e.Message}\r\n{e.StackTrace}");
            throw new Exception($"Loading FFmpeg libraries '{Engine.Config.FFmpegPath}' failed");
        }
    }

    internal static void SetLogLevel()
    {
        if (Engine.Config.FFmpegLogLevel != Flyleaf.FFmpeg.LogLevel.Quiet)
        {
            av_log_set_level(Engine.Config.FFmpegLogLevel);
            av_log_set_callback(LogFFmpeg);
        }
        else
        {
            av_log_set_level(Flyleaf.FFmpeg.LogLevel.Quiet);
            av_log_set_callback(null);
        }
    }

    internal unsafe static av_log_set_callback_callback LogFFmpeg = (p0, level, format, vl) =>
    {
        if (level > av_log_get_level())
            return;

        byte*   buffer = stackalloc byte[AV_LOG_BUFFER_SIZE];
        int     printPrefix = 1;
        av_log_format_line2(p0, level, format, vl, buffer, AV_LOG_BUFFER_SIZE, &printPrefix);
        string  line = Utils.BytePtrToStringUTF8(buffer);

        Logger.Output($"FFmpeg|{level,-7}|{line.Trim()}");
    };

    internal unsafe static string ErrorCodeToMsg(int error)
    {
        byte* buffer = stackalloc byte[AV_LOG_BUFFER_SIZE];
        av_strerror(error, buffer, AV_LOG_BUFFER_SIZE);
        return Utils.BytePtrToStringUTF8(buffer);
    }
}

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FlyleafLib;

public class WhisperLanguage : IEquatable<WhisperLanguage>
{
    public string Code { get; set; }
    public string EnglishName { get; set; }

    // https://github.com/ggerganov/whisper.cpp/blob/d682e150908e10caa4c15883c633d7902d385237/src/whisper.cpp#L248-L348
    private static readonly Dictionary<string, string> CodeToLanguage = new()
    {
        {"en", "english"},
        {"zh", "chinese"},
        {"de", "german"},
        {"es", "spanish"},
        {"ru", "russian"},
        {"ko", "korean"},
        {"fr", "french"},
        {"ja", "japanese"},
        {"pt", "portuguese"},
        {"tr", "turkish"},
        {"pl", "polish"},
        {"ca", "catalan"},
        {"nl", "dutch"},
        {"ar", "arabic"},
        {"sv", "swedish"},
        {"it", "italian"},
        {"id", "indonesian"},
        {"hi", "hindi"},
        {"fi", "finnish"},
        {"vi", "vietnamese"},
        {"he", "hebrew"},
        {"uk", "ukrainian"},
        {"el", "greek"},
        {"ms", "malay"},
        {"cs", "czech"},
        {"ro", "romanian"},
        {"da", "danish"},
        {"hu", "hungarian"},
        {"ta", "tamil"},
        {"no", "norwegian"},
        {"th", "thai"},
        {"ur", "urdu"},
        {"hr", "croatian"},
        {"bg", "bulgarian"},
        {"lt", "lithuanian"},
        {"la", "latin"},
        {"mi", "maori"},
        {"ml", "malayalam"},
        {"cy", "welsh"},
        {"sk", "slovak"},
        {"te", "telugu"},
        {"fa", "persian"},
        {"lv", "latvian"},
        {"bn", "bengali"},
        {"sr", "serbian"},
        {"az", "azerbaijani"},
        {"sl", "slovenian"},
        {"kn", "kannada"},
        {"et", "estonian"},
        {"mk", "macedonian"},
        {"br", "breton"},
        {"eu", "basque"},
        {"is", "icelandic"},
        {"hy", "armenian"},
        {"ne", "nepali"},
        {"mn", "mongolian"},
        {"bs", "bosnian"},
        {"kk", "kazakh"},
        {"sq", "albanian"},
        {"sw", "swahili"},
        {"gl", "galician"},
        {"mr", "marathi"},
        {"pa", "punjabi"},
        {"si", "sinhala"},
        {"km", "khmer"},
        {"sn", "shona"},
        {"yo", "yoruba"},
        {"so", "somali"},
        {"af", "afrikaans"},
        {"oc", "occitan"},
        {"ka", "georgian"},
        {"be", "belarusian"},
        {"tg", "tajik"},
        {"sd", "sindhi"},
        {"gu", "gujarati"},
        {"am", "amharic"},
        {"yi", "yiddish"},
        {"lo", "lao"},
        {"uz", "uzbek"},
        {"fo", "faroese"},
        {"ht", "haitian creole"},
        {"ps", "pashto"},
        {"tk", "turkmen"},
        {"nn", "nynorsk"},
        {"mt", "maltese"},
        {"sa", "sanskrit"},
        {"lb", "luxembourgish"},
        {"my", "myanmar"},
        {"bo", "tibetan"},
        {"tl", "tagalog"},
        {"mg", "malagasy"},
        {"as", "assamese"},
        {"tt", "tatar"},
        {"haw", "hawaiian"},
        {"ln", "lingala"},
        {"ha", "hausa"},
        {"ba", "bashkir"},
        {"jw", "javanese"},
        {"su", "sundanese"},
        {"yue", "cantonese"},
    };

    // https://github.com/Purfview/whisper-standalone-win/issues/430#issuecomment-2743029023
    // https://github.com/openai/whisper/blob/517a43ecd132a2089d85f4ebc044728a71d49f6e/whisper/tokenizer.py#L10-L110
    public static Dictionary<string, string> LanguageToCode { get; } = CodeToLanguage.ToDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase);

    public static List<WhisperLanguage> GetWhisperLanguages()
    {
        return CodeToLanguage.Select(kv => new WhisperLanguage
        {
            Code = kv.Key,
            EnglishName = UpperFirstOfWords(kv.Value)
        }).OrderBy(l => l.EnglishName).ToList();
    }

    private static string UpperFirstOfWords(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        StringBuilder result = new();
        bool isNewWord = true;

        foreach (char c in input)
        {
            if (char.IsWhiteSpace(c))
            {
                isNewWord = true;
                result.Append(c);
            }
            else if (isNewWord)
            {
                result.Append(char.ToUpper(c));
                isNewWord = false;
            }
            else
            {
                result.Append(c);
            }
        }

        return result.ToString();
    }

    public bool Equals(WhisperLanguage other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        return Code == other.Code;
    }

    public override bool Equals(object obj) => obj is WhisperLanguage o && Equals(o);

    public override int GetHashCode()
    {
        return (Code != null ? Code.GetHashCode() : 0);
    }
}

using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.ViewModels;

public class SettingsDialogVM : Bindable, IDialogAware
{
    public FlyleafManager FL { get; }
    public SettingsDialogVM(FlyleafManager fl)
    {
        FL = fl;
    }

    public DelegateCommand<string>? CmdCloseDialog => field ??= new((parameter) =>
    {
        ButtonResult result = ButtonResult.None;

        if (parameter == "Save")
        {
            result = ButtonResult.OK;
        }

        RequestClose.Invoke(result);
    });

    #region IDialogAware
    public string Title { get; set => Set(ref field, value); } = $"Settings - {App.Name}";
    public double WindowWidth { get; set => Set(ref field, value); } = 1000;
    public double WindowHeight { get; set => Set(ref field, value); } = 700;

    public bool CanCloseDialog() => true;

    public void OnDialogClosed()
    {
    }

    public void OnDialogOpened(IDialogParameters parameters)
    {
    }

    public DialogCloseListener RequestClose { get; }
    #endregion
}

using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using FlyleafLib.MediaPlayer.Translation;
using FlyleafLib.MediaPlayer.Translation.Services;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitlesTrans : UserControl
{
    public SettingsSubtitlesTrans()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsSubtitlesTransVM>();
    }
}

public class SettingsSubtitlesTransVM : Bindable
{
    public FlyleafManager FL { get; }

    public SettingsSubtitlesTransVM(FlyleafManager fl)
    {
        FL = fl;

        SelectedTranslateServiceType = FL.PlayerConfig.Subtitles.TranslateServiceType;
    }

    public TranslateServiceType SelectedTranslateServiceType
    {
        get;
        set
        {
            Set(ref field, value);

            FL.PlayerConfig.Subtitles.TranslateServiceType = value;

            if (FL.PlayerConfig.Subtitles.TranslateServiceSettings.TryGetValue(value, out var settings))
            {
                // It points to an instance of the same class, so change this will be reflected in the config.
                SelectedServiceSettings = settings;
            }
            else
            {
                ITranslateSettings? defaultSettings = value.DefaultSettings();
                FL.PlayerConfig.Subtitles.TranslateServiceSettings.Add(value, defaultSettings);

                SelectedServiceSettings = defaultSettings;
            }
        }
    }

    public ITranslateSettings? SelectedServiceSettings { get; set => Set(ref field, value); }

    public DelegateCommand? CmdSetDefaultPromptKeepContext => field ??= new(() =>
    {
        FL.PlayerConfig.Subtitles.TranslateChatConfig.PromptKeepContext = TranslateChatConfig.DefaultPromptKeepContext.ReplaceLineEndings("\n");
    });

    public DelegateCommand? CmdSetDefaultPromptOneByOne => field ??= new(() =>
    {
        FL.PlayerConfig.Subtitles.TranslateChatConfig.PromptOneByOne = TranslateChatConfig.DefaultPromptOneByOne.ReplaceLineEndings("\n");
    });
}

[ValueConversion(typeof(TargetLanguage), typeof(string))]
internal class TargetLanguageEnumToStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is TargetLanguage enumValue)
        {
            return enumValue.DisplayName();
        }
        return value?.ToString() ?? string.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(TranslateServiceType), typeof(string))]
internal class TranslateServiceTypeEnumToStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is TranslateServiceType enumValue)
        {
            string displayName = enumValue.GetDescription();

            if (enumValue.IsLLM())
            {
                return $"{displayName} (LLM)";
            }

            return $"{displayName}";
        }

        return value?.ToString() ?? string.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(TranslateServiceType), typeof(string))]
internal class TranslateServiceTypeEnumToUrlConverter : IValueConverter
{
    private const string BaseUrl = "https://github.com/umlx5h/LLPlayer/wiki/Translation-Engine";
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is TranslateServiceType enumValue)
        {
            string displayName = enumValue.GetDescription();

            return $"{BaseUrl}#{displayName.ToLower().Replace(' ', '-')}";
        }
        return BaseUrl;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(TargetLanguage), typeof(string))]
internal class TargetLanguageEnumToNoSupportedTranslateServiceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is TargetLanguage enumValue)
        {
            TranslateServiceType supported = enumValue.SupportedServiceType();

            // DeepL = DeepLX
            List<TranslateServiceType> notSupported =
               Enum.GetValues<TranslateServiceType>()
                    .Where(t => t != TranslateServiceType.DeepLX)
                    .Where(t => !supported.HasFlag(t))
                    .ToList();

            if (notSupported.Count == 0)
            {
                return "[All supported]";
            }

            return string.Join(',', notSupported.Select(t => t.ToString()));
        }
        return value?.ToString() ?? string.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaFrame;
using static FlyleafLib.Utils;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

partial class Player
{
    string stoppedWithError = null;

    /// <summary>
    /// Fires on playback stopped by an error or completed / ended successfully <see cref="Status"/>
    /// Warning: Uses Invoke and it comes from playback thread so you can't pause/stop etc. You need to use another thread if you have to.
    /// </summary>
    public event EventHandler<PlaybackStoppedArgs> PlaybackStopped;
    protected virtual void OnPlaybackStopped(string error = null)
    {
        if (error != null && LastError == null)
        {
            lastError = error;
            UI(() => LastError = LastError);
        }

        PlaybackStopped?.Invoke(this, new PlaybackStoppedArgs(error));
    }

    /// <summary>
    /// Fires on seek completed for the specified ms (ms will be -1 on failure)
    /// </summary>
    public event EventHandler<int> SeekCompleted;

    /// <summary>
    /// Plays AVS streams
    /// </summary>
    public void Play()
    {
        lock (lockActions)
        {
            if (!CanPlay || Status == Status.Playing || Status == Status.Ended)
                return;

            status = Status.Playing;
            UI(() => Status = Status);
        }

        while (taskPlayRuns || taskSeekRuns) Thread.Sleep(5);
        taskPlayRuns = true;

        Thread t = new(() =>
        {
            try
            {
                Engine.TimeBeginPeriod1();
                NativeMethods.SetThreadExecutionState(NativeMethods.EXECUTION_STATE.ES_CONTINUOUS | NativeMethods.EXECUTION_STATE.ES_SYSTEM_REQUIRED | NativeMethods.EXECUTION_STATE.ES_DISPLAY_REQUIRED);

                onBufferingStarted   = 0;
                onBufferingCompleted = 0;
                requiresBuffering    = true;

                if (LastError != null)
                {
                    lastError = null;
                    UI(() => LastError = LastError);
                }

                if (Config.Player.Usage == Usage.Audio || !Video.IsOpened)
                    ScreamerAudioOnly();
                else
                {
                    if (ReversePlayback)
                    {
                        shouldFlushNext = true;
                        ScreamerReverse();
                    }
                    else
                    {
                        shouldFlushPrev = true;
                        Screamer();
                    }

                }

            }
            catch (Exception ex)
            {
                Log.Error($"Playback failed ({ex.Message})");
                RaiseUnknownErrorOccurred($"Playback failed: {ex.Message}", UnknownErrorType.Playback, ex);
            }
            finally
            {
                VideoDecoder.DisposeFrame(vFrame);
                vFrame = null;
                for (int i = 0; i < subNum; i++)
                {
                    sFrames[i] = null;
                }

                if (Status == Status.Stopped)
                    decoder?.Initialize();
                else if (decoder != null)
                {
                    decoder.PauseOnQueueFull();
                    decoder.PauseDecoders();
                }

                Audio.ClearBuffer();
                Engine.TimeEndPeriod1();
                NativeMethods.SetThreadExecutionState(NativeMethods.EXECUTION_STATE.ES_CONTINUOUS);
                stoppedWithError = null;

                if (IsPlaying)
                {
                    if (decoderHasEnded)
                        status = Status.Ended;
                    else
                    {
                        if (Video.IsOpened && VideoDemuxer.Interrupter.Timedout)
                            stoppedWithError = "Timeout";
                        else if (onBufferingStarted - 1 == onBufferingCompleted)
                        {
                            stoppedWithError = "Playback stopped unexpectedly";
                            OnBufferingCompleted("Buffering failed");
                        }
                        else
                        {
                            if (!ReversePlayback)
                            {
                                if (isLive || Math.Abs(Duration - CurTime) > 3 * 1000 * 10000)
                                    stoppedWithError = "Playback stopped unexpectedly";
                            }
                            else if (CurTime > 3 * 1000 * 10000)
                                stoppedWithError = "Playback stopped unexpectedly";
                        }

                        status = Status.Paused;
                    }
                }

                OnPlaybackStopped(stoppedWithError);
                if (CanDebug) Log.Debug($"[SCREAMER] Finished (Status: {Status}, Error: {stoppedWithError})");

                UI(() =>
                {
                    Status = Status;
                    UpdateCurTime();
                });

                taskPlayRuns = false;
            }
        });
        t.Priority = Config.Player.ThreadPriority;
        t.Name = $"[#{PlayerId}] Playback";
        t.IsBackground = true;
        t.Start();
    }

    /// <summary>
    /// Pauses AVS streams
    /// </summary>
    public void Pause()
    {
        lock (lockActions)
        {
            if (!CanPlay || Status == Status.Ended)
                return;

            status = Status.Paused;
            UI(() => Status = Status);

            while (taskPlayRuns) Thread.Sleep(5);
        }
    }

    public void TogglePlayPause()
    {
        if (IsPlaying)
            Pause();
        else
            Play();
    }

    public void ToggleReversePlayback()
        => ReversePlayback = !ReversePlayback;

    public void ToggleLoopPlayback()
        => LoopPlayback = !LoopPlayback;

    /// <summary>
    /// Seeks backwards or forwards based on the specified ms to the nearest keyframe
    /// </summary>
    /// <param name="ms"></param>
    /// <param name="forward"></param>
    public void Seek(int ms, bool forward = false) => Seek(ms, forward, false);

    /// <summary>
    /// Seeks at the exact timestamp (with half frame distance accuracy)
    /// </summary>
    /// <param name="time"></param>
    /// <param name="subIndex"></param>
    public void SeekAccurate(TimeSpan time, int subIndex = -1)
    {
        int ms = (int)time.TotalMilliseconds;
        if (subIndex != -1 && time.Microseconds > 0)
        {
            // When seeking subtitles, rounding down the milliseconds or less will cause the previous subtitle
            // ASR subtitles are in microseconds, so if you truncate, you'll end up one before the current subtitle.
            ms += 1;
        }

        SeekAccurate(ms, subIndex);
    }

    /// <summary>
    /// Seeks at the exact timestamp (with half frame distance accuracy)
    /// </summary>
    /// <param name="ms"></param>
    /// <param name="subIndex"></param>
    public void SeekAccurate(int ms, int subIndex = -1)
    {
        if (subIndex >= 0)
        {
            ms += (int)(Config.Subtitles[subIndex].Delay / 10000);
        }

        Seek(ms, false, !IsLive);
    }

    public void ToggleSeekAccurate()
        => Config.Player.SeekAccurate = !Config.Player.SeekAccurate;

    private void Seek(int ms, bool forward, bool accurate)
    {
        if (!CanPlay)
            return;

        lock (seeks)
        {
            _CurTime = curTime = ms * (long)10000;
            seeks.Push(new SeekData(ms, forward, accurate));
        }

        // Set timestamp to Manager immediately after seek to enable continuous seek
        // (Without this, seek is called with the same timestamp on successive calls, so it will seek to the same subtitle.)
        SubtitlesManager.SetCurrentTime(new TimeSpan(curTime));

        Raise(nameof(CurTime));
        Raise(nameof(RemainingDuration));

        if (Status == Status.Playing)
            return;

        lock (seeks)
        {
            if (taskSeekRuns)
                return;

            taskSeekRuns = true;
        }

        Task.Run(() =>
        {
            int ret;
            bool wasEnded = false;
            SeekData seekData = null;

            try
            {
                Engine.TimeBeginPeriod1();

                while (true)
                {
                    lock (seeks)
                    {
                        if (!(seeks.TryPop(out seekData) && CanPlay && !IsPlaying))
                        {
                            taskSeekRuns = false;
                            break;
                        }

                        seeks.Clear();
                    }

                    if (Status == Status.Ended)
                    {
                        wasEnded = true;
                        status = Status.Paused;
                        UI(() => Status = Status);
                    }

                    for (int i = 0; i < subNum; i++)
                    {
                        // Display subtitles from cache when seeking while paused
                        bool display = false;
                        var cur = SubtitlesManager[i].GetCurrent();
                        if (cur != null)
                        {
                            if (!string.IsNullOrEmpty(cur.DisplayText))
                            {
                                SubtitleDisplay(cur.DisplayText, i, cur.UseTranslated);
                                display = true;
                            }
                            else if (cur.IsBitmap && cur.Bitmap != null)
                            {
                                SubtitleDisplay(cur.Bitmap, i);
                                display = true;
                            }

                            if (display)
                            {
                                sFramesPrev[i] = new SubtitlesFrame
                                {
                                    timestamp = cur.StartTime.Ticks + Config.Subtitles[i].Delay,
                                    duration = (uint)cur.Duration.TotalMilliseconds,
                                    isTranslated = cur.UseTranslated
                                };
                            }
                        }

                        // clear subtitles
                        // but do not clear when cache hit
                        if (!display && sFramesPrev[i] != null)
                        {
                            sFramesPrev[i] = null;
                            SubtitleClear(i);
                        }
                    }

                    if (!Video.IsOpened)
                    {
                        if (AudioDecoder.OnVideoDemuxer)
                        {
                            ret = decoder.Seek(seekData.ms, seekData.forward);
                            if (CanWarn && ret < 0)
                                Log.Warn("Seek failed 2");

                            VideoDemuxer.Start();
                            SeekCompleted?.Invoke(this, -1);
                        }
                        else
                        {
                            ret = decoder.SeekAudio(seekData.ms, seekData.forward);
                            if (CanWarn && ret < 0)
                                Log.Warn("Seek failed 3");

                            AudioDemuxer.Start();
                            SeekCompleted?.Invoke(this, -1);
                        }

                        decoder.PauseOnQueueFull();
                        SeekCompleted?.Invoke(this, seekData.ms);
                    }
                    else
                    {
                        decoder.PauseDecoders();
                        ret = decoder.Seek(seekData.accurate ? Math.Max(0, seekData.ms - (int) new TimeSpan(Config.Player.SeekAccurateFixMargin).TotalMilliseconds) : seekData.ms, seekData.forward, !seekData.accurate); // 3sec ffmpeg bug for seek accurate when fails to seek backwards (see videodecoder getframe)
                        if (ret < 0)
                        {
                            if (CanWarn) Log.Warn("Seek failed");
                            SeekCompleted?.Invoke(this, -1);
                        }
                        else if (!ReversePlayback && CanPlay)
                        {
                            decoder.GetVideoFrame(seekData.accurate ? seekData.ms * (long)10000 : -1);
                            ShowOneFrame();
                            VideoDemuxer.Start();
                            AudioDemuxer.Start();
                            //for (int i = 0; i < subNum; i++)
                            //{
                            //    SubtitlesDemuxers[i].Start();
                            //}
                            DataDemuxer.Start();
                            decoder.PauseOnQueueFull();
                            SeekCompleted?.Invoke(this, seekData.ms);
                        }
                    }

                    Thread.Sleep(20);
                }
            }
            catch (Exception e)
            {
                lock (seeks) taskSeekRuns = false;
                Log.Error($"Seek failed ({e.Message})");
            }
            finally
            {
                decoder.OpenedPlugin?.OnBufferingCompleted();
                Engine.TimeEndPeriod1();
                if ((wasEnded && Config.Player.AutoPlay) || stoppedWithError != null) // TBR: Possible race condition with if (Status == Status.Playing)
                    Play();
            }
        });
    }

    /// <summary>
    /// Flushes the buffer (demuxers (packets) and decoders (frames))
    /// This is useful mainly for live streams to push the playback at very end (low latency)
    /// </summary>
    public void Flush()
    {
        decoder.Flush();
        OSDMessage = "Buffer Flushed";
    }

    /// <summary>
    /// Stops and Closes AVS streams
    /// </summary>
    public void Stop()
    {
        lock (lockActions)
        {
            Initialize();
            renderer?.Flush();
        }
    }
    public void SubtitleClear()
    {
        for (int i = 0; i < subNum; i++)
        {
            SubtitleClear(i);
        }
    }

    public void SubtitleClear(int subIndex)
    {
        Subtitles[subIndex].Data.Clear();
        //renderer.ClearOverlayTexture();
    }

    /// <summary>
    /// Updated text format subtitle display
    /// </summary>
    /// <param name="text"></param>
    /// <param name="subIndex"></param>
    /// <param name="isTranslated"></param>
    public void SubtitleDisplay(string text, int subIndex, bool isTranslated)
    {
        UI(() =>
        {
            Subtitles[subIndex].Data.IsTranslated = isTranslated;
            Subtitles[subIndex].Data.Language = isTranslated
                ? Config.Subtitles.TranslateLanguage
                : SubtitlesManager[subIndex].Language;

            Subtitles[subIndex].Data.Text = text;
            Subtitles[subIndex].Data.Bitmap = null;
        });
    }

    public void SubtitleDisplay(SubtitleBitmapData bitmapData, int subIndex)
    {
        if (bitmapData.Sub.num_rects == 0)
        {
            return;
        }

        (byte[] data, AVSubtitleRect rect) = bitmapData.SubToBitmap(false);

        SubtitlesFrameBitmap bitmap = new()
        {
            data = data,
            width = rect.w,
            height = rect.h,
            x = rect.x,
            y = rect.y,
        };

        SubtitleDisplay(bitmap, subIndex);
    }

    /// <summary>
    /// Update bitmap format subtitle display
    /// </summary>
    /// <param name="bitmap"></param>
    /// <param name="subIndex"></param>
    public void SubtitleDisplay(SubtitlesFrameBitmap bitmap, int subIndex)
    {
        // TODO: L: refactor

        // Each subtitle has a different size and needs to be generated each time.
        WriteableBitmap wb = new(
            bitmap.width, bitmap.height,
            NativeMethods.DpiXSource, NativeMethods.DpiYSource,
            PixelFormats.Bgra32, null
        );
        Int32Rect rect = new(0, 0, bitmap.width, bitmap.height);
        wb.Lock();

        Marshal.Copy(bitmap.data, 0, wb.BackBuffer, bitmap.data.Length);

        wb.AddDirtyRect(rect);
        wb.Unlock();
        // Note that you will get a UI thread error if you don't call
        wb.Freeze();

        int x = bitmap.x;
        int y = bitmap.y;
        int w = bitmap.width;
        int h = bitmap.height;

        SubsBitmap subsBitmap = new()
        {
            X = x,
            Y = y,
            Width = w,
            Height = h,
            Source = wb,
        };

        UI(() =>
        {
            Subtitles[subIndex].Data.Bitmap = subsBitmap;
            Subtitles[subIndex].Data.Text = "";
        });
    }
}

public class PlaybackStoppedArgs : EventArgs
{
    public string   Error       { get; }
    public bool     Success     { get; }

    public PlaybackStoppedArgs(string error)
    {
        Error   = error;
        Success = Error == null;
    }
}

class SeekData
{
    public int  ms;
    public bool forward;
    public bool accurate;
    public SeekData(int ms, bool forward, bool accurate)
        { this.ms = ms; this.forward = forward && !accurate; this.accurate = accurate; }
}

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDecoder;

public unsafe class SubtitlesDecoder : DecoderBase
{
    public SubtitlesStream  SubtitlesStream     => (SubtitlesStream) Stream;

    public ConcurrentQueue<SubtitlesFrame>
                            Frames              { get; protected set; } = new ConcurrentQueue<SubtitlesFrame>();

    public PacketQueue      SubtitlesPackets;

    public SubtitlesDecoder(Config config, int uniqueId = -1, int subIndex = 0) : base(config, uniqueId)
    {
        this.subIndex = subIndex;
    }

    private readonly int subIndex;
    protected override int Setup(AVCodec* codec)
    {
        lock (lockCodecCtx)
        {
            if (demuxer.avioCtx != null)
            {
                // Disable check since already converted to UTF-8
                codecCtx->sub_charenc_mode = SubCharencModeFlags.Ignore;
            }
        }

        return 0;
    }

    protected override void DisposeInternal()
        => DisposeFrames();

    public void Flush()
    {
        lock (lockActions)
        lock (lockCodecCtx)
        {
            if (Disposed) return;

            if (Status == Status.Ended) Status = Status.Stopped;
            //else if (Status == Status.Draining) Status = Status.Stopping;

            DisposeFrames();
            avcodec_flush_buffers(codecCtx);
        }
    }

    protected override void RunInternal()
    {
        int ret = 0;
        int allowedErrors = Config.Decoder.MaxErrors;
        AVPacket *packet;

        SubtitlesPackets = demuxer.SubtitlesPackets[subIndex];

        do
        {
            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxSubsFrames)
            {
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxSubsFrames && Status == Status.QueueFull) Thread.Sleep(20);

                lock (lockStatus)
                {
                    if (Status != Status.QueueFull) break;
                    Status = Status.Running;
                }
            }

            // While Packets Queue Empty (Ended | Quit if Demuxer stopped | Wait until we get packets)
            if (SubtitlesPackets.Count == 0)
            {
                CriticalArea = true;

                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueEmpty;

                while (SubtitlesPackets.Count == 0 && Status == Status.QueueEmpty)
                {
                    if (demuxer.Status == Status.Ended)
                    {
                        Status = Status.Ended;
                        break;
                    }
                    else if (!demuxer.IsRunning)
                    {
                        if (CanDebug) Log.Debug($"Demuxer is not running [Demuxer Status: {demuxer.Status}]");

                        int retries = 5;

                        while (retries > 0)
                        {
                            retries--;
                            Thread.Sleep(10);
                            if (demuxer.IsRunning) break;
                        }

                        lock (demuxer.lockStatus)
                        lock (lockStatus)
                        {
                            if (demuxer.Status == Status.Pausing || demuxer.Status == Status.Paused)
                                Status = Status.Pausing;
                            else if (demuxer.Status != Status.Ended)
                                Status = Status.Stopping;
                            else
                                continue;
                        }

                        break;
                    }

                    Thread.Sleep(20);
                }

                lock (lockStatus)
                {
                    CriticalArea = false;
                    if (Status != Status.QueueEmpty) break;
                    Status = Status.Running;
                }
            }

            lock (lockCodecCtx)
            {
                if (Status == Status.Stopped || SubtitlesPackets.Count == 0) continue;
                packet = SubtitlesPackets.Dequeue();

                int gotFrame = 0;
                SubtitlesFrame subFrame = new();

                fixed(AVSubtitle* subPtr = &subFrame.sub)
                    ret = avcodec_decode_subtitle2(codecCtx, subPtr, &gotFrame, packet);

                if (ret < 0)
                {
                    allowedErrors--;
                    if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                    if (allowedErrors == 0) { Log.Error("Too many errors!"); Status = Status.Stopping; break; }

                    continue;
                }

                if (gotFrame == 0)
                {
                    av_packet_free(&packet);
                    continue;
                }

                long pts = subFrame.sub.pts != AV_NOPTS_VALUE ? subFrame.sub.pts /*mcs*/ * 10 : (packet->pts != AV_NOPTS_VALUE ? (long)(packet->pts * SubtitlesStream.Timebase) : AV_NOPTS_VALUE);
                av_packet_free(&packet);

                if (pts == AV_NOPTS_VALUE)
                    continue;

                pts += subFrame.sub.start_display_time /*ms*/ * 10000L;

                if (!filledFromCodec) // TODO: CodecChanged? And when findstreaminfo is disabled as it is an external demuxer will not know the main demuxer's start time
                {
                    filledFromCodec = true;
                    avcodec_parameters_from_context(Stream.AVStream->codecpar, codecCtx);
                    SubtitlesStream.Refresh();

                    CodecChanged?.Invoke(this);
                }

                if (subFrame.sub.num_rects < 1)
                {
                    if (SubtitlesStream.IsBitmap) // clear prev subs frame
                    {
                        subFrame.duration   = uint.MaxValue;
                        subFrame.timestamp  = pts - demuxer.StartTime + Config.Subtitles[subIndex].Delay;
                        subFrame.isBitmap   = true;
                        Frames.Enqueue(subFrame);
                    }

                    fixed(AVSubtitle* subPtr = &subFrame.sub)
                        avsubtitle_free(subPtr);

                    continue;
                }

                subFrame.duration   = subFrame.sub.end_display_time;
                subFrame.timestamp  = pts - demuxer.StartTime + Config.Subtitles[subIndex].Delay;

                if (subFrame.sub.rects[0]->type == AVSubtitleType.Ass)
                {
                    subFrame.text = Utils.BytePtrToStringUTF8(subFrame.sub.rects[0]->ass).Trim();
                    Config.Subtitles.Parser(subFrame);

                    fixed(AVSubtitle* subPtr = &subFrame.sub)
                        avsubtitle_free(subPtr);

                    if (string.IsNullOrEmpty(subFrame.text))
                        continue;
                }
                else if (subFrame.sub.rects[0]->type == AVSubtitleType.Text)
                {
                    subFrame.text = Utils.BytePtrToStringUTF8(subFrame.sub.rects[0]->text).Trim();

                    fixed(AVSubtitle* subPtr = &subFrame.sub)
                        avsubtitle_free(subPtr);

                    if (string.IsNullOrEmpty(subFrame.text))
                        continue;
                }
                else if (subFrame.sub.rects[0]->type == AVSubtitleType.Bitmap)
                {
                    var rect = subFrame.sub.rects[0];
                    byte[] data = Renderer.ConvertBitmapSub(subFrame.sub, false);

                    subFrame.isBitmap = true;
                    subFrame.bitmap = new SubtitlesFrameBitmap()
                    {
                        data = data,
                        width = rect->w,
                        height = rect->h,
                        x = rect->x,
                        y = rect->y,
                    };
                }

                if (CanTrace) Log.Trace($"Processes {Utils.TicksToTime(subFrame.timestamp)}");

                Frames.Enqueue(subFrame);
            }
        } while (Status == Status.Running);
    }

    public static void DisposeFrame(SubtitlesFrame frame)
    {
        Debug.Assert(frame != null, "frame is already disposed (race condition)");
        if (frame != null && frame.sub.num_rects > 0)
            fixed(AVSubtitle* ptr = &frame.sub)
                avsubtitle_free(ptr);
    }

    public void DisposeFrames()
    {
        if (!SubtitlesStream.IsBitmap)
            Frames = new ConcurrentQueue<SubtitlesFrame>();
        else
        {
            while (!Frames.IsEmpty)
            {
                Frames.TryDequeue(out var frame);
                DisposeFrame(frame);
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfColorFontDialog
{
    /// <summary>
    /// Interaction logic for ColorFontChooser.xaml
    /// </summary>
    public partial class ColorFontChooser : UserControl
    {
        public FontInfo SelectedFont
        {
            get
            {
                return new FontInfo(this.txtSampleText.FontFamily, this.txtSampleText.FontSize, this.txtSampleText.FontStyle, this.txtSampleText.FontStretch, this.txtSampleText.FontWeight, this.colorPicker.SelectedColor.Brush);
            }
        }

        public bool ShowColorPicker
        {
            get { return (bool)GetValue(ShowColorPickerProperty); }
            set { SetValue(ShowColorPickerProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ShowColorPicker.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ShowColorPickerProperty =
            DependencyProperty.Register("ShowColorPicker", typeof(bool), typeof(ColorFontChooser), new PropertyMetadata(true, ShowColorPickerPropertyCallback));

        public bool AllowArbitraryFontSizes
        {
            get { return (bool)GetValue(AllowArbitraryFontSizesProperty); }
            set { SetValue(AllowArbitraryFontSizesProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AllowArbitraryFontSizes.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AllowArbitraryFontSizesProperty =
            DependencyProperty.Register("AllowArbitraryFontSizes", typeof(bool), typeof(ColorFontChooser), new PropertyMetadata(true, AllowArbitraryFontSizesPropertyCallback));

        public bool PreviewFontInFontList
        {
            get { return (bool)GetValue(PreviewFontInFontListProperty); }
            set { SetValue(PreviewFontInFontListProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PreviewFontInFontList.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PreviewFontInFontListProperty =
            DependencyProperty.Register("PreviewFontInFontList", typeof(bool), typeof(ColorFontChooser), new PropertyMetadata(true, PreviewFontInFontListPropertyCallback));

        public ColorFontChooser()
        {
            InitializeComponent();
            this.groupBoxColorPicker.Visibility = ShowColorPicker ? Visibility.Visible : Visibility.Collapsed;
            this.tbFontSize.IsEnabled = AllowArbitraryFontSizes;
            lstFamily.ItemTemplate = PreviewFontInFontList ? (DataTemplate)Resources["fontFamilyData"] : (DataTemplate)Resources["fontFamilyDataWithoutPreview"];
        }
        private static void PreviewFontInFontListPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorFontChooser chooser = d as ColorFontChooser;
            if (e.NewValue == null)
                return;
            if ((bool)e.NewValue == true)
                chooser.lstFamily.ItemTemplate = chooser.Resources["fontFamilyData"] as DataTemplate;
            else
                chooser.lstFamily.ItemTemplate = chooser.Resources["fontFamilyDataWithoutPreview"] as DataTemplate;
        }
        private static void AllowArbitraryFontSizesPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorFontChooser chooser = d as ColorFontChooser;
            if (e.NewValue == null)
                return;

            chooser.tbFontSize.IsEnabled = (bool)e.NewValue;

        }
        private static void ShowColorPickerPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorFontChooser chooser = d as ColorFontChooser;
            if (e.NewValue == null)
                return;
            if ((bool)e.NewValue == true)
                chooser.groupBoxColorPicker.Visibility = Visibility.Visible;
            else
                chooser.groupBoxColorPicker.Visibility = Visibility.Collapsed;
        }

        private void colorPicker_ColorChanged(object sender, RoutedEventArgs e)
        {
            this.txtSampleText.Foreground = this.colorPicker.SelectedColor.Brush;
        }

        private void lstFontSizes_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (this.tbFontSize != null && this.lstFontSizes.SelectedItem != null)
            {
                this.tbFontSize.Text = this.lstFontSizes.SelectedItem.ToString();
            }
        }

        private void tbFontSize_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            e.Handled = !TextBoxTextAllowed(e.Text);
        }

        private void tbFontSize_Pasting(object sender, DataObjectPastingEventArgs e)
        {
            if (e.DataObject.GetDataPresent(typeof(String)))
            {
                String Text1 = (String)e.DataObject.GetData(typeof(String));
                if (!TextBoxTextAllowed(Text1)) e.CancelCommand();
            }
            else
            {
                e.CancelCommand();
            }
        }
        private Boolean TextBoxTextAllowed(String Text2)
        {
            return Array.TrueForAll<Char>(Text2.ToCharArray(),
                delegate (Char c) { return Char.IsDigit(c) || Char.IsControl(c); });
        }

        private void tbFontSize_LostFocus(object sender, RoutedEventArgs e)
        {
            if (tbFontSize.Text.Length == 0)
            {
                if (this.lstFontSizes.SelectedItem == null)
                {
                    lstFontSizes.SelectedIndex = 0;
                }
                tbFontSize.Text = this.lstFontSizes.SelectedItem.ToString();

            }
        }

        private void tbFontSize_TextChanged(object sender, TextChangedEventArgs e)
        {
            foreach (int size in this.lstFontSizes.Items)
            {
                if (size.ToString() == tbFontSize.Text)
                {
                    lstFontSizes.SelectedItem = size;
                    lstFontSizes.ScrollIntoView(size);
                    return;
                }
            }
            this.lstFontSizes.SelectedItem = null;
        }
    }
}

using System.Collections.Generic;
using System.Collections.ObjectModel;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaFramework.MediaPlaylist;

public class PlaylistItem : DemuxerInput
{
    public int      Index                   { get; set; } = -1; // if we need it we need to ensure we fix it in case of removing an item

    /// <summary>
    /// While the Url can expire or be null DirectUrl can be used as a new input for re-opening
    /// </summary>
    public string   DirectUrl               { get; set; }

    /// <summary>
    /// Relative folder to playlist's folder base (can be empty, not null)
    /// Use Path.Combine(Playlist.FolderBase, Folder) to get absolute path for saving related files with the current selection item (such as subtitles)
    /// </summary>
    public string   Folder                  { get; set; } = "";

    public long     FileSize                { get; set; }

    /// <summary>
    /// Usually just the filename part of the provided Url
    /// </summary>
    public string   OriginalTitle           { get => _OriginalTitle;set => SetUI(ref _OriginalTitle, value ?? "", false); }
    string _OriginalTitle = "";

    /// <summary>
    /// Movie/TVShow Title
    /// </summary>
    public string   MediaTitle              { get => _MediaTitle;   set => SetUI(ref _MediaTitle, value ?? "", false); }
    string _MediaTitle = "";

    /// <summary>
    /// Movie/TVShow Title including Movie's Year or TVShow's Season/Episode
    /// </summary>
    public string   Title                   { get => _Title;        set { if (_Title == "") OriginalTitle = value; SetUI(ref _Title, value ?? "", false);} }
    string _Title = "";

    public List<Demuxer.Chapter>
                    Chapters                { get; set; } = new();

    public int      Season                  { get; set; }
    public int      Episode                 { get; set; }
    public int      Year                    { get; set; }

    public Dictionary<string, object>
                    Tag                     { get; set; } = [];
    public void AddTag(object tag, string pluginName)
    {
        if (!Tag.TryAdd(pluginName, tag))
            Tag[pluginName] = tag;
    }

    public object GetTag(string pluginName)
        => Tag.TryGetValue(pluginName, out object value) ? value : null;

    public bool     SearchedLocal           { get; set; }
    public bool     SearchedOnline          { get; set; }

    /// <summary>
    /// Whether the item is currently enabled or not
    /// </summary>
    public bool     Enabled                 { get => _Enabled; set { if (SetUI(ref _Enabled, value) && value == true) OpenedCounter++; } }
    bool _Enabled;
    public int      OpenedCounter           { get; set; }

    public ExternalVideoStream
                    ExternalVideoStream     { get; set; }
    public ExternalAudioStream
                    ExternalAudioStream     { get; set; }
    public ExternalSubtitlesStream[]
                    ExternalSubtitlesStreams
                                            { get; set; } = new ExternalSubtitlesStream[2];

    public ObservableCollection<ExternalVideoStream>
                    ExternalVideoStreams    { get; set; } = [];
    public ObservableCollection<ExternalAudioStream>
                    ExternalAudioStreams    { get; set; } = [];
    public ObservableCollection<ExternalSubtitlesStream>
                    ExternalSubtitlesStreamsAll
                                            { get; set; } = [];
    internal object lockExternalStreams = new();

    bool filled;
    public void FillMediaParts() // Called during OpenScrape (if file) & Open/Search Subtitles (to be able to search online and compare tvshow/movie properly)
    {
        if (filled)
            return;

        filled      = true;
        var mp      = Utils.GetMediaParts(OriginalTitle);
        Year        = mp.Year;
        Season      = mp.Season;
        Episode     = mp.Episode;
        MediaTitle  = mp.Title; // safe title to check with online subs

        if (mp.Season > 0 && mp.Episode > 0) // tvshow
        {
            var title = "S";
            title += Season > 9 ? Season : $"0{Season}";
            title += "E";
            title += Episode > 9 ? Episode : $"0{Episode}";

            Title = mp.Title == "" ? title : mp.Title + " (" + title + ")";
        }
        else if (mp.Year > 0) // movie
            Title = mp.Title + " (" + mp.Year + ")";
    }

    public static void AddExternalStream(ExternalStream extStream, PlaylistItem item, string pluginName, object tag = null)
    {
        lock (item.lockExternalStreams)
        {
            extStream.PlaylistItem = item;
            extStream.PluginName = pluginName;

            if (extStream is ExternalAudioStream astream)
            {
                item.ExternalAudioStreams.Add(astream);
                extStream.Index = item.ExternalAudioStreams.Count - 1;
            }
            else if (extStream is ExternalVideoStream vstream)
            {
                item.ExternalVideoStreams.Add(vstream);
                extStream.Index = item.ExternalVideoStreams.Count - 1;
            }
            else if (extStream is ExternalSubtitlesStream sstream)
            {
                item.ExternalSubtitlesStreamsAll.Add(sstream);
                extStream.Index = item.ExternalSubtitlesStreamsAll.Count - 1;
            }

            if (tag != null)
                extStream.AddTag(tag, pluginName);
        };
    }
}

using System.Collections.Generic;
using System.Drawing;
using System.Globalization;

namespace FlyleafLib.MediaFramework.MediaFrame;

public class SubtitlesFrame : FrameBase
{
    public bool isBitmap;
    public uint duration;
    public string text;
    public List<SubStyle> subStyles;
    public AVSubtitle sub;
    public SubtitlesFrameBitmap bitmap;

    // for translation switch
    public bool isTranslated;
}

public class SubtitlesFrameBitmap
{
    // Buffer to hold decoded pixels
    public byte[] data;
    public int width;
    public int height;
    public int x;
    public int y;
}

public struct SubStyle
{
    public SubStyles style;
    public Color value;

    public int from;
    public int len;

    public SubStyle(int from, int len, Color value) : this(SubStyles.COLOR, from, len, value) { }
    public SubStyle(SubStyles style, int from = -1, int len = -1, Color? value = null)
    {
        this.style  = style;
        this.value  = value == null ? Color.White : (Color)value;
        this.from   = from;
        this.len    = len;
    }
}

public enum SubStyles
{
    BOLD,
    ITALIC,
    UNDERLINE,
    STRIKEOUT,
    FONTSIZE,
    FONTNAME,
    COLOR
}

/// <summary>
/// Default Subtitles Parser
/// </summary>
public static class ParseSubtitles
{
    public static void Parse(SubtitlesFrame sFrame)
    {
        sFrame.text = SSAtoSubStyles(sFrame.text, out var subStyles);
        sFrame.subStyles = subStyles;
    }
    public static string SSAtoSubStyles(string s, out List<SubStyle> styles)
    {
        int pos = 0;
        string sout = "";
        styles = new List<SubStyle>();

        SubStyle bold       = new(SubStyles.BOLD);
        SubStyle italic     = new(SubStyles.ITALIC);
        SubStyle underline  = new(SubStyles.UNDERLINE);
        SubStyle strikeout  = new(SubStyles.STRIKEOUT);
        SubStyle color      = new(SubStyles.COLOR);

        //SubStyle fontname      = new SubStyle(SubStyles.FONTNAME);
        //SubStyle fontsize      = new SubStyle(SubStyles.FONTSIZE);

        if (string.IsNullOrEmpty(s))
        {
            return sout;
        }

        s = s.LastIndexOf(",,") == -1 ? s : s[(s.LastIndexOf(",,") + 2)..].Replace("\\N", "\n").Trim();

        for (int i = 0; i < s.Length; i++)
        {
            if (s[i] == '{') continue;

            if (s[i] == '\\' && s[i - 1] == '{')
            {
                int codeLen = s.IndexOf('}', i) - i;
                if (codeLen == -1) continue;

                string code = s.Substring(i, codeLen).Trim();

                switch (code[1])
                {
                    case 'c':
                        if (code.Length == 2)
                        {
                            if (color.from == -1) break;

                            color.len = pos - color.from;
                            if (color.value != Color.Transparent)
                                styles.Add(color);

                            color = new SubStyle(SubStyles.COLOR);
                        }
                        else
                        {
                            color.from = pos;
                            color.value = Color.Transparent;
                            if (code.Length < 7) break;

                            int colorEnd = code.LastIndexOf('&');
                            if (colorEnd < 6) break;

                            string hexColor = code[4..colorEnd];

                            int red = int.Parse(hexColor.Substring(hexColor.Length - 2, 2), NumberStyles.HexNumber);
                            int green = 0;
                            int blue = 0;

                            if (hexColor.Length - 2 > 0)
                            {
                                hexColor = hexColor[..^2];
                                if (hexColor.Length == 1)
                                    hexColor = "0" + hexColor;

                                green = int.Parse(hexColor.Substring(hexColor.Length - 2, 2), NumberStyles.HexNumber);
                            }

                            if (hexColor.Length - 2 > 0)
                            {
                                hexColor = hexColor[..^2];
                                if (hexColor.Length == 1)
                                    hexColor = "0" + hexColor;

                                blue = int.Parse(hexColor.Substring(hexColor.Length - 2, 2), NumberStyles.HexNumber);
                            }

                            color.value = Color.FromArgb(255, red, green, blue);
                        }
                        break;

                    case 'b':
                        if (code[2] == '0')
                        {
                            if (bold.from == -1) break;

                            bold.len = pos - bold.from;
                            styles.Add(bold);
                            bold = new SubStyle(SubStyles.BOLD);
                        }
                        else
                        {
                            bold.from = pos;
                            //bold.value = code.Substring(2, code.Length-2);
                        }

                        break;

                    case 'u':
                        if (code[2] == '0')
                        {
                            if (underline.from == -1) break;

                            underline.len = pos - underline.from;
                            styles.Add(underline);
                            underline = new SubStyle(SubStyles.UNDERLINE);
                        }
                        else
                        {
                            underline.from = pos;
                        }

                        break;

                    case 's':
                        if (code[2] == '0')
                        {
                            if (strikeout.from == -1) break;

                            strikeout.len = pos - strikeout.from;
                            styles.Add(strikeout);
                            strikeout = new SubStyle(SubStyles.STRIKEOUT);
                        }
                        else
                        {
                            strikeout.from = pos;
                        }

                        break;

                    case 'i':
                        if (code.Length > 2 && code[2] == '0')
                        {
                            if (italic.from == -1) break;

                            italic.len = pos - italic.from;
                            styles.Add(italic);
                            italic = new SubStyle(SubStyles.ITALIC);
                        }
                        else
                        {
                            italic.from = pos;
                        }

                        break;
                }

                i += codeLen;
                continue;
            }

            sout += s[i];
            pos++;
        }

        // Non-Closing Codes
        int soutPostLast = sout.Length;
        if (bold.from != -1) { bold.len = soutPostLast - bold.from; styles.Add(bold); }
        if (italic.from != -1) { italic.len = soutPostLast - italic.from; styles.Add(italic); }
        if (strikeout.from != -1) { strikeout.len = soutPostLast - strikeout.from; styles.Add(strikeout); }
        if (underline.from != -1) { underline.len = soutPostLast - underline.from; styles.Add(underline); }
        if (color.from != -1 && color.value != Color.Transparent) { color.len = soutPostLast - color.from; styles.Add(color); }

        // Greek issue?
        sout = sout.Replace("ʼ", "Ά");

        return sout;
    }
}

using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using System.Windows;

using FlyleafLib.MediaFramework.MediaDevice;
using FlyleafLib.MediaPlayer;

namespace FlyleafLib;

/// <summary>
/// Flyleaf Engine
/// </summary>
public static class Engine
{
    /// <summary>
    /// Engine has been loaded and is ready for use
    /// </summary>
    public static bool              IsLoaded        { get; private set; }

    /// <summary>
    /// Engine's configuration
    /// </summary>
    public static EngineConfig      Config          { get; private set; }

    /// <summary>
    /// Audio Engine
    /// </summary>
    public static AudioEngine       Audio           { get; private set; }

    /// <summary>
    /// Video Engine
    /// </summary>
    public static VideoEngine       Video           { get; private set; }

    /// <summary>
    /// Plugins Engine
    /// </summary>
    public static PluginsEngine     Plugins         { get; private set; }

    /// <summary>
    /// FFmpeg Engine
    /// </summary>
    public static FFmpegEngine      FFmpeg          { get; private set; }

    /// <summary>
    /// List of active Players
    /// </summary>
    public static List<Player>      Players         { get; private set; }

    public static event EventHandler
                    Loaded;

    internal static LogHandler
                    Log;

    static Thread   tMaster;
    static object   lockEngine = new();
    static bool     isLoading;
    static int      timePeriod;

    /// <summary>
    /// Initializes Flyleaf's Engine (Must be called from UI thread)
    /// </summary>
    /// <param name="config">Engine's configuration</param>
    public static void Start(EngineConfig config = null) => StartInternal(config);

    /// <summary>
    /// Initializes Flyleaf's Engine Async (Must be called from UI thread)
    /// </summary>
    /// <param name="config">Engine's configuration</param>
    public static void StartAsync(EngineConfig config = null) => StartInternal(config, true);

    /// <summary>
    /// Requests timeBeginPeriod(1) - You should call TimeEndPeriod1 when not required anymore
    /// </summary>
    public static void TimeBeginPeriod1()
    {
        lock (lockEngine)
        {
            timePeriod++;

            if (timePeriod == 1)
            {
                Log.Trace("timeBeginPeriod(1)");
                Utils.NativeMethods.TimeBeginPeriod(1);
            }
        }
    }

    /// <summary>
    /// Stops previously requested timeBeginPeriod(1)
    /// </summary>
    public static void TimeEndPeriod1()
    {
        lock (lockEngine)
        {
            timePeriod--;

            if (timePeriod == 0)
            {
                Log.Trace("timeEndPeriod(1)");
                Utils.NativeMethods.TimeEndPeriod(1);
            }
        }
    }

    private static void StartInternal(EngineConfig config = null, bool async = false)
    {
        lock (lockEngine)
        {
            if (isLoading)
                return;

            isLoading = true;

            Config = config ?? new EngineConfig();

            if (Application.Current == null)
                new Application();

            StartInternalUI();

            if (async)
                Task.Run(() => StartInternalNonUI());
            else
                StartInternalNonUI();
        }
    }

    private static void StartInternalUI()
    {
        Application.Current.Exit += (o, e) =>
        {
            Config.UIRefresh = false;
            Config.UIRefreshInterval = 1;

            while (Players.Count != 0)
                Players[0].Dispose();
        };

        Logger.SetOutput();

        Log = new LogHandler("[FlyleafEngine] ");

        Audio = new AudioEngine();
        if (Config.FFmpegLoadProfile == LoadProfile.All)
            AudioDevice.RefreshDevices();
    }

    private static void StartInternalNonUI()
    {
        var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
        Log.Info($"FlyleafLib {version.Major }.{version.Minor}.{version.Build}");

        FFmpeg  = new FFmpegEngine();
        Video   = new VideoEngine();
        if (Config.FFmpegLoadProfile == LoadProfile.All)
            VideoDevice.RefreshDevices();
        Plugins = new PluginsEngine();
        Players = new List<Player>();

        IsLoaded = true;
        Loaded?.Invoke(null, null);

        if (Config.UIRefresh)
            StartThread();
    }
    internal static void AddPlayer(Player player)
    {
        lock (Players)
            Players.Add(player);
    }
    internal static int  GetPlayerPos(int playerId)
    {
        for (int i=0; i<Players.Count; i++)
            if (Players[i].PlayerId == playerId)
                return i;

        return -1;
    }
    internal static void DisposePlayer(Player player)
    {
        if (player == null) return;

        DisposePlayer(player.PlayerId);
    }
    internal static void DisposePlayer(int playerId)
    {
        lock (Players)
        {
            Log.Trace($"Disposing {playerId}");
            int pos = GetPlayerPos(playerId);
            if (pos == -1) return;

            var player = Players[pos];
            player.DisposeInternal();
            Players.RemoveAt(pos);
            Log.Trace($"Disposed {playerId}");
        }
    }

    internal static void StartThread()
    {
        if (!Config.UIRefresh || (tMaster != null && tMaster.IsAlive))
            return;

        tMaster = new Thread(() => { MasterThread(); })
        {
            Name = "FlyleafEngine",
            IsBackground = true
        };
        tMaster.Start();
    }
    internal static void MasterThread()
    {
        Log.Info("Thread started");

        int curLoop     = 0;
        int secondLoops = 1000 / Config.UIRefreshInterval;
        long prevTicks  = DateTime.UtcNow.Ticks;
        double curSecond= 0;

        do
        {
            try
            {
                if (Players.Count == 0)
                {
                    Thread.Sleep(Config.UIRefreshInterval);
                    continue;
                }

                curLoop++;
                if (curLoop == secondLoops)
                {
                    long curTicks = DateTime.UtcNow.Ticks;
                    curSecond = (curTicks - prevTicks) / 10000000.0;
                    prevTicks = curTicks;
                }

                lock (Players)
                    foreach (var player in Players)
                    {
                        /* Every UIRefreshInterval */
                        player.Activity.RefreshMode();

                        /* Every Second */
                        if (curLoop == secondLoops)
                        {
                            if (player.Config.Player.Stats)
                            {
                                var curStats = player.stats;
                                // TODO: L: including Subtitles bytes?
                                long curTotalBytes  = player.VideoDemuxer.TotalBytes + player.AudioDemuxer.TotalBytes;
                                long curVideoBytes  = player.VideoDemuxer.VideoPackets.Bytes + player.AudioDemuxer.VideoPackets.Bytes;
                                long curAudioBytes  = player.VideoDemuxer.AudioPackets.Bytes + player.AudioDemuxer.AudioPackets.Bytes;

                                player.bitRate      = (curTotalBytes - curStats.TotalBytes) * 8 / 1000.0;
                                player.Video.bitRate= (curVideoBytes - curStats.VideoBytes) * 8 / 1000.0;
                                player.Audio.bitRate= (curAudioBytes - curStats.AudioBytes) * 8 / 1000.0;

                                curStats.TotalBytes = curTotalBytes;
                                curStats.VideoBytes = curVideoBytes;
                                curStats.AudioBytes = curAudioBytes;

                                if (player.IsPlaying)
                                {
                                    player.Video.fpsCurrent = (player.Video.FramesDisplayed - curStats.FramesDisplayed) / curSecond;
                                    curStats.FramesDisplayed = player.Video.FramesDisplayed;
                                }
                            }
                        }
                    }

                if (curLoop == secondLoops)
                    curLoop = 0;

                Action action = () =>
                {
                    try
                    {
                        foreach (var player in Players)
                        {
                            /* Every UIRefreshInterval */

                            // Activity Mode Refresh & Hide Mouse Cursor (FullScreen only)
                            if (player.Activity.mode != player.Activity._Mode)
                                player.Activity.SetMode();

                            // CurTime / Buffered Duration (+Duration for HLS)
                            if (!Config.UICurTimePerSecond)
                                player.UpdateCurTime();
                            else if (player.Status == Status.Paused)
                            {
                                if (player.MainDemuxer.IsRunning)
                                    player.UpdateCurTime();
                                else
                                    player.UpdateBufferedDuration();
                            }

                            /* Every Second */
                            if (curLoop == 0)
                            {
                                // Stats Refresh (BitRates / FrameDisplayed / FramesDropped / FPS)
                                if (player.Config.Player.Stats)
                                {
                                    player.BitRate = player.BitRate;
                                    player.Video.BitRate = player.Video.BitRate;
                                    player.Audio.BitRate = player.Audio.BitRate;

                                    if (player.IsPlaying)
                                    {
                                        player.Audio.FramesDisplayed= player.Audio.FramesDisplayed;
                                        player.Audio.FramesDropped  = player.Audio.FramesDropped;

                                        player.Video.FramesDisplayed= player.Video.FramesDisplayed;
                                        player.Video.FramesDropped  = player.Video.FramesDropped;
                                        player.Video.FPSCurrent     = player.Video.FPSCurrent;
                                    }
                                }
                            }
                        }
                    } catch { }
                };

                Utils.UI(action);
                Thread.Sleep(Config.UIRefreshInterval);

            } catch { curLoop = 0; }

        } while (Config.UIRefresh);

        Log.Info("Thread stopped");
    }
}

using System;
using System.Collections.ObjectModel;

using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaPlayer;

public class Video : NotifyPropertyChanged
{
    /// <summary>
    /// Embedded Streams
    /// </summary>
    public ObservableCollection<VideoStream>
                        Streams         => decoder?.VideoDemuxer.VideoStreams;

    /// <summary>
    /// Whether the input has video and it is configured
    /// </summary>
    public bool         IsOpened        { get => isOpened;          internal set => Set(ref _IsOpened, value); }
    internal bool   _IsOpened, isOpened;

    public string       Codec           { get => codec;             internal set => Set(ref _Codec, value); }
    internal string _Codec, codec;

    ///// <summary>
    ///// Video bitrate (Kbps)
    ///// </summary>
    public double       BitRate         { get => bitRate;           internal set => Set(ref _BitRate, value); }
    internal double _BitRate, bitRate;

    public AspectRatio  AspectRatio     { get => aspectRatio;       internal set => Set(ref _AspectRatio, value); }
    internal AspectRatio
                    _AspectRatio, aspectRatio;

    ///// <summary>
    ///// Total Dropped Frames
    ///// </summary>
    public int          FramesDropped   { get => framesDropped;     internal set => Set(ref _FramesDropped, value); }
    internal int    _FramesDropped, framesDropped;

    /// <summary>
    /// Total Frames
    /// </summary>
    public int          FramesTotal     { get => framesTotal;       internal set => Set(ref _FramesTotal, value); }
    internal int    _FramesTotal, framesTotal;

    public int          FramesDisplayed { get => framesDisplayed;   internal set => Set(ref _FramesDisplayed, value); }
    internal int    _FramesDisplayed, framesDisplayed;

    public double       FPS             { get => fps;               internal set => Set(ref _FPS, value); }
    internal double _FPS, fps;

    /// <summary>
    /// Actual Frames rendered per second (FPS)
    /// </summary>
    public double       FPSCurrent      { get => fpsCurrent;        internal set => Set(ref _FPSCurrent, value); }
    internal double _FPSCurrent, fpsCurrent;

    public string       PixelFormat     { get => pixelFormat;       internal set => Set(ref _PixelFormat, value); }
    internal string _PixelFormat, pixelFormat;

    public int          Width           { get => width;             internal set => Set(ref _Width, value); }
    internal int    _Width, width;

    public int          Height          { get => height;            internal set => Set(ref _Height, value); }
    internal int    _Height, height;

    public bool         VideoAcceleration
                                                { get => videoAcceleration; internal set => Set(ref _VideoAcceleration, value); }
    internal bool   _VideoAcceleration, videoAcceleration;

    public bool         ZeroCopy        { get => zeroCopy;          internal set => Set(ref _ZeroCopy, value); }
    internal bool   _ZeroCopy, zeroCopy;

    public Player Player => player;

    Action uiAction;
    Player player;
    DecoderContext decoder => player.decoder;
    Config Config => player.Config;

    public Video(Player player)
    {
        this.player = player;

        uiAction = () =>
        {
            IsOpened            = IsOpened;
            Codec               = Codec;
            AspectRatio         = AspectRatio;
            FramesTotal         = FramesTotal;
            FPS                 = FPS;
            PixelFormat         = PixelFormat;
            Width               = Width;
            Height              = Height;
            VideoAcceleration   = VideoAcceleration;
            ZeroCopy            = ZeroCopy;

            FramesDisplayed     = FramesDisplayed;
            FramesDropped       = FramesDropped;
        };
    }

    internal void Reset()
    {
        codec              = null;
        aspectRatio        = new AspectRatio(0, 0);
        bitRate            = 0;
        fps                = 0;
        pixelFormat        = null;
        width              = 0;
        height             = 0;
        framesTotal        = 0;
        videoAcceleration  = false;
        zeroCopy           = false;
        isOpened           = false;

        player.UIAdd(uiAction);
    }
    internal void Refresh()
    {
        if (decoder.VideoStream == null) { Reset(); return; }

        codec       = decoder.VideoStream.Codec;
        aspectRatio = decoder.VideoStream.AspectRatio;
        fps         = decoder.VideoStream.FPS;
        pixelFormat = decoder.VideoStream.PixelFormatStr;
        width       = (int)decoder.VideoStream.Width;
        height      = (int)decoder.VideoStream.Height;
        framesTotal = decoder.VideoStream.TotalFrames;
        videoAcceleration
                    = decoder.VideoDecoder.VideoAccelerated;
        zeroCopy    = decoder.VideoDecoder.ZeroCopy;
        isOpened    =!decoder.VideoDecoder.Disposed;

        framesDisplayed = 0;
        framesDropped   = 0;

        player.UIAdd(uiAction);
    }

    internal void Enable()
    {
        if (player.VideoDemuxer.Disposed || Config.Player.Usage == Usage.Audio)
            return;

        bool wasPlaying = player.IsPlaying;

        player.Pause();
        decoder.OpenSuggestedVideo();
        player.ReSync(decoder.VideoStream, (int) (player.CurTime / 10000), true);

        if (wasPlaying || Config.Player.AutoPlay)
            player.Play();
    }
    internal void Disable()
    {
        if (!IsOpened)
            return;

        bool wasPlaying = player.IsPlaying;

        player.Pause();
        decoder.CloseVideo();
        player.SubtitleClear();

        if (!player.Audio.IsOpened)
        {
            player.canPlay = false;
            player.UIAdd(() => player.CanPlay = player.CanPlay);
        }

        Reset();
        player.UIAll();

        if (wasPlaying || Config.Player.AutoPlay)
            player.Play();
    }

    public void Toggle() => Config.Video.Enabled = !Config.Video.Enabled;
    public void ToggleKeepRatio()
    {
        if (Config.Video.AspectRatio == AspectRatio.Keep)
            Config.Video.AspectRatio = AspectRatio.Fill;
        else if (Config.Video.AspectRatio == AspectRatio.Fill)
            Config.Video.AspectRatio = AspectRatio.Keep;
    }
    public void ToggleVideoAcceleration() => Config.Video.VideoAcceleration = !Config.Video.VideoAcceleration;
}

using System;
using System.Drawing.Imaging;
using System.IO;
using System.Windows;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaRenderer;

using static FlyleafLib.Utils;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

unsafe partial class Player
{
    public bool IsOpenFileDialogOpen    { get; private set; }

    public void SeekBackward()  => SeekBackward_(Config.Player.SeekOffset, Config.Player.SeekOffsetAccurate);
    public void SeekBackward2() => SeekBackward_(Config.Player.SeekOffset2, Config.Player.SeekOffsetAccurate2);
    public void SeekBackward3() => SeekBackward_(Config.Player.SeekOffset3, Config.Player.SeekOffsetAccurate3);
    public void SeekBackward4() => SeekBackward_(Config.Player.SeekOffset4, Config.Player.SeekOffsetAccurate4);
    public void SeekBackward_(long offset, bool accurate)
    {
        if (!CanPlay)
            return;

        long seekTs = CurTime - (CurTime % offset) - offset;

        if (Config.Player.SeekAccurate || accurate)
            SeekAccurate(Math.Max((int) (seekTs / 10000), 0));
        else
            Seek(Math.Max((int) (seekTs / 10000), 0), false);
    }

    public void SeekForward()   => SeekForward_(Config.Player.SeekOffset, Config.Player.SeekOffsetAccurate);
    public void SeekForward2()  => SeekForward_(Config.Player.SeekOffset2, Config.Player.SeekOffsetAccurate2);
    public void SeekForward3()  => SeekForward_(Config.Player.SeekOffset3, Config.Player.SeekOffsetAccurate3);
    public void SeekForward4()  => SeekForward_(Config.Player.SeekOffset4, Config.Player.SeekOffsetAccurate4);
    public void SeekForward_(long offset, bool accurate)
    {
        if (!CanPlay)
            return;

        long seekTs = CurTime - (CurTime % offset) + offset;

        if (seekTs > Duration && !isLive)
            return;

        if (Config.Player.SeekAccurate || accurate)
            SeekAccurate((int)(seekTs / 10000));
        else
            Seek((int)(seekTs / 10000), true);
    }

    public void SeekToChapter(Demuxer.Chapter chapter) =>
        /* TODO
* Accurate pts required (backward/forward check)
* Get current chapter implementation + next/prev
*/
        Seek((int)(chapter.StartTime / 10000.0), true);

    public void CopyToClipboard()
    {
        var url = decoder.Playlist.Url;
        if (url == null)
            return;

        Clipboard.SetText(url);
        OSDMessage = $"Copy {url}";
    }
    public void CopyItemToClipboard()
    {
        if (decoder.Playlist.Selected == null || decoder.Playlist.Selected.DirectUrl == null)
            return;

        string url = decoder.Playlist.Selected.DirectUrl;

        Clipboard.SetText(url);
        OSDMessage = $"Copy {url}";
    }
    public void OpenFromClipboard()
    {
        string text = Clipboard.GetText();
        if (!string.IsNullOrWhiteSpace(text))
        {
            OpenAsync(text);
        }
    }

    public void OpenFromClipboardSafe()
    {
        if (decoder.Playlist.Selected != null)
        {
            return;
        }

        OpenFromClipboard();
    }

    public void OpenFromFileDialog()
    {
        int prevTimeout = Activity.Timeout;
        Activity.IsEnabled = false;
        Activity.Timeout = 0;
        IsOpenFileDialogOpen = true;

        System.Windows.Forms.OpenFileDialog openFileDialog = new();

        // If there is currently an open file, set that folder as the base folder
        if (decoder.Playlist.Url != null && File.Exists(decoder.Playlist.Url))
        {
            var folder = Path.GetDirectoryName(decoder.Playlist.Url);
            if (folder != null)
                openFileDialog.InitialDirectory = folder;
        }

        var res = openFileDialog.ShowDialog();

        if (res == System.Windows.Forms.DialogResult.OK)
            OpenAsync(openFileDialog.FileName);

        Activity.Timeout = prevTimeout;
        Activity.IsEnabled = true;
        IsOpenFileDialogOpen = false;
    }

    public void ShowFrame(int frameIndex)
    {
        if (!Video.IsOpened || !CanPlay || VideoDemuxer.IsHLSLive) return;

        lock (lockActions)
        {
            Pause();
            dFrame = null;
            for (int i = 0; i < subNum; i++)
            {
                sFrames[i] = null;
                SubtitleClear(i);
            }

            decoder.Flush();
            decoder.RequiresResync = true;

            vFrame = VideoDecoder.GetFrame(frameIndex);
            if (vFrame == null) return;

            if (CanDebug) Log.Debug($"SFI: {VideoDecoder.GetFrameNumber(vFrame.timestamp)}");

            curTime = vFrame.timestamp;
            renderer.Present(vFrame);
            reversePlaybackResync = true;
            vFrame = null;

            UI(() => UpdateCurTime());
        }
    }

    // Whether video queue should be flushed as it could have opposite direction frames
    bool shouldFlushNext;
    bool shouldFlushPrev;
    public void ShowFrameNext()
    {
        if (!Video.IsOpened || !CanPlay || VideoDemuxer.IsHLSLive)
            return;

        lock (lockActions)
        {
            Pause();

            if (Status == Status.Ended)
            {
                status = Status.Paused;
                UI(() => Status = Status);
            }

            shouldFlushPrev = true;
            decoder.RequiresResync = true;

            if (shouldFlushNext)
            {
                decoder.StopThreads();
                decoder.Flush();
                shouldFlushNext = false;

                var vFrame = VideoDecoder.GetFrame(VideoDecoder.GetFrameNumber(CurTime));
                VideoDecoder.DisposeFrame(vFrame);
            }

            for (int i = 0; i < subNum; i++)
            {
                sFrames[i] = null;
                SubtitleClear(i);
            }

            if (VideoDecoder.Frames.IsEmpty)
                vFrame = VideoDecoder.GetFrameNext();
            else
                VideoDecoder.Frames.TryDequeue(out vFrame);

            if (vFrame == null)
                return;

            if (CanDebug) Log.Debug($"SFN: {VideoDecoder.GetFrameNumber(vFrame.timestamp)}");

            curTime = curTime = vFrame.timestamp;
            renderer.Present(vFrame);
            reversePlaybackResync = true;
            vFrame = null;

            UI(() => UpdateCurTime());
        }
    }
    public void ShowFramePrev()
    {
        if (!Video.IsOpened || !CanPlay || VideoDemuxer.IsHLSLive)
            return;

        lock (lockActions)
        {
            Pause();

            if (Status == Status.Ended)
            {
                status = Status.Paused;
                UI(() => Status = Status);
            }

            shouldFlushNext = true;
            decoder.RequiresResync = true;

            if (shouldFlushPrev)
            {
                decoder.StopThreads();
                decoder.Flush();
                shouldFlushPrev = false;
            }

            for (int i = 0; i < subNum; i++)
            {
                sFrames[i] = null;
                SubtitleClear(i);
            }

            if (VideoDecoder.Frames.IsEmpty)
            {
                // Temp fix for previous timestamps until we seperate GetFrame for Extractor and the Player
                reversePlaybackResync = true;
                int askedFrame = VideoDecoder.GetFrameNumber(CurTime) - 1;
                //Log.Debug($"CurTime1: {TicksToTime(CurTime)}, Asked: {askedFrame}");
                vFrame = VideoDecoder.GetFrame(askedFrame);
                if (vFrame == null) return;

                int recvFrame = VideoDecoder.GetFrameNumber(vFrame.timestamp);
                //Log.Debug($"CurTime2: {TicksToTime(vFrame.timestamp)}, Got: {recvFrame}");
                if (askedFrame != recvFrame)
                {
                    VideoDecoder.DisposeFrame(vFrame);
                    vFrame = null;
                    vFrame = askedFrame > recvFrame
                        ? VideoDecoder.GetFrame(VideoDecoder.GetFrameNumber(CurTime))
                        : VideoDecoder.GetFrame(VideoDecoder.GetFrameNumber(CurTime) - 2);
                }
            }
            else
                VideoDecoder.Frames.TryDequeue(out vFrame);

            if (vFrame == null)
                return;

            if (CanDebug) Log.Debug($"SFB: {VideoDecoder.GetFrameNumber(vFrame.timestamp)}");

            curTime = vFrame.timestamp;
            renderer.Present(vFrame);
            vFrame = null;
            UI(() => UpdateCurTime()); // For some strange reason this will not be updated on KeyDown (only on KeyUp) which doesn't happen on ShowFrameNext (GPU overload? / Thread.Sleep underlying in UI thread?)
        }
    }

    public void SpeedUp()       => Speed += Config.Player.SpeedOffset;
    public void SpeedUp2()      => Speed += Config.Player.SpeedOffset2;
    public void SpeedDown()     => Speed -= Config.Player.SpeedOffset;
    public void SpeedDown2()    => Speed -= Config.Player.SpeedOffset2;

    public void RotateRight()   => renderer.Rotation = (renderer.Rotation + 90) % 360;
    public void RotateLeft()    => renderer.Rotation = renderer.Rotation < 90 ? 360 + renderer.Rotation - 90 : renderer.Rotation - 90;

    public void FullScreen()    => Host?.Player_SetFullScreen(true);
    public void NormalScreen()  => Host?.Player_SetFullScreen(false);
    public void ToggleFullScreen()
    {
        if (Host == null)
            return;

        if (Host.Player_GetFullScreen())
            Host.Player_SetFullScreen(false);
        else
            Host.Player_SetFullScreen(true);
    }

    /// <summary>
    /// Starts recording (uses Config.Player.FolderRecordings and default filename title_curTime)
    /// </summary>
    public void StartRecording()
    {
        if (!CanPlay)
            return;
        try
        {
            if (!Directory.Exists(Config.Player.FolderRecordings))
                Directory.CreateDirectory(Config.Player.FolderRecordings);

            string filename = GetValidFileName(string.IsNullOrEmpty(Playlist.Selected.Title) ? "Record" : Playlist.Selected.Title) + $"_{new TimeSpan(CurTime):hhmmss}." + decoder.Extension;
            filename = FindNextAvailableFile(Path.Combine(Config.Player.FolderRecordings, filename));
            StartRecording(ref filename, false);
        } catch { }
    }

    /// <summary>
    /// Starts recording
    /// </summary>
    /// <param name="filename">Path of the new recording file</param>
    /// <param name="useRecommendedExtension">You can force the output container's format or use the recommended one to avoid incompatibility</param>
    public void StartRecording(ref string filename, bool useRecommendedExtension = true)
    {
        if (!CanPlay)
            return;

        OSDMessage = $"Start recording to {Path.GetFileName(filename)}";
        decoder.StartRecording(ref filename, useRecommendedExtension);
        IsRecording = decoder.IsRecording;
    }

    /// <summary>
    /// Stops recording
    /// </summary>
    public void StopRecording()
    {
        decoder.StopRecording();
        IsRecording = decoder.IsRecording;
        OSDMessage = "Stop recording";
    }
    public void ToggleRecording()
    {
        if (!CanPlay) return;

        if (IsRecording)
            StopRecording();
        else
            StartRecording();
    }

    /// <summary>
    /// <para>Saves the current video frame (encoding based on file extention .bmp, .png, .jpg)</para>
    /// <para>If filename not specified will use Config.Player.FolderSnapshots and with default filename title_frameNumber.ext (ext from Config.Player.SnapshotFormat)</para>
    /// <para>If width/height not specified will use the original size. If one of them will be set, the other one will be set based on original ratio</para>
    /// <para>If frame not specified will use the current/last frame</para>
    /// </summary>
    /// <param name="filename">Specify the filename (null: will use Config.Player.FolderSnapshots and with default filename title_frameNumber.ext (ext from Config.Player.SnapshotFormat)</param>
    /// <param name="width">Specify the width (-1: will keep the ratio based on height)</param>
    /// <param name="height">Specify the height (-1: will keep the ratio based on width)</param>
    /// <param name="frame">Specify the frame (null: will use the current/last frame)</param>
    /// <exception cref="Exception"></exception>
    public void TakeSnapshotToFile(string filename = null, int width = -1, int height = -1, VideoFrame frame = null)
    {
        if (!CanPlay)
            return;

        if (filename == null)
        {
            try
            {
                if (!Directory.Exists(Config.Player.FolderSnapshots))
                    Directory.CreateDirectory(Config.Player.FolderSnapshots);

                // TBR: if frame is specified we don't know the frame's number
                filename = GetValidFileName(string.IsNullOrEmpty(Playlist.Selected.Title) ? "Snapshot" : Playlist.Selected.Title) + $"_{(frame == null ? VideoDecoder.GetFrameNumber(CurTime).ToString() : "X")}.{Config.Player.SnapshotFormat}";
                filename = FindNextAvailableFile(Path.Combine(Config.Player.FolderSnapshots, filename));
            } catch { return; }
        }

        string ext = GetUrlExtention(filename);

        ImageFormat imageFormat;

        switch (ext)
        {
            case "bmp":
                imageFormat = ImageFormat.Bmp;
                break;

            case "png":
                imageFormat = ImageFormat.Png;
                break;

            case "jpg":
            case "jpeg":
                imageFormat = ImageFormat.Jpeg;
                break;

            default:
                throw new Exception($"Invalid snapshot extention '{ext}' (valid .bmp, .png, .jpeg, .jpg");
        }

        if (renderer == null)
            return;

        var snapshotBitmap = renderer.GetBitmap(width, height, frame);
        if (snapshotBitmap == null)
            return;

        Exception e = null;
        try
        {
            snapshotBitmap.Save(filename, imageFormat);

            UI(() =>
            {
                OSDMessage = $"Save snapshot to {Path.GetFileName(filename)}";
            });
        }
        catch (Exception e2)
        {
            e = e2;
        }
        snapshotBitmap.Dispose();

        if (e != null)
            throw e;
    }

    /// <summary>
    /// <para>Returns a bitmap of the current or specified video frame</para>
    /// <para>If width/height not specified will use the original size. If one of them will be set, the other one will be set based on original ratio</para>
    /// <para>If frame not specified will use the current/last frame</para>
    /// </summary>
    /// <param name="width">Specify the width (-1: will keep the ratio based on height)</param>
    /// <param name="height">Specify the height (-1: will keep the ratio based on width)</param>
    /// <param name="frame">Specify the frame (null: will use the current/last frame)</param>
    /// <returns></returns>
    public System.Drawing.Bitmap TakeSnapshotToBitmap(int width = -1, int height = -1, VideoFrame frame = null) => renderer?.GetBitmap(width, height, frame);

    public void ZoomIn()         => Zoom += Config.Player.ZoomOffset;
    public void ZoomOut()       { if (Zoom - Config.Player.ZoomOffset < 1) return; Zoom -= Config.Player.ZoomOffset; }

    /// <summary>
    /// Pan zoom in with center point
    /// </summary>
    /// <param name="p"></param>
    public void ZoomIn(Point p) { renderer.ZoomWithCenterPoint(p, renderer.Zoom + Config.Player.ZoomOffset / 100.0); RaiseUI(nameof(Zoom)); }

    /// <summary>
    /// Pan zoom out with center point
    /// </summary>
    /// <param name="p"></param>
    public void ZoomOut(Point p){ double zoom = renderer.Zoom - Config.Player.ZoomOffset / 100.0; if (zoom < 0.001) return; renderer.ZoomWithCenterPoint(p, zoom); RaiseUI(nameof(Zoom)); }

    /// <summary>
    /// Pan zoom (no raise)
    /// </summary>
    /// <param name="zoom"></param>
    public void SetZoom(double zoom) => renderer.SetZoom(zoom);
    /// <summary>
    /// Pan zoom's center point (no raise, no center point change)
    /// </summary>
    /// <param name="p"></param>
    public void SetZoomCenter(Point p) => renderer.SetZoomCenter(p);
    /// <summary>
    /// Pan zoom and center point (no raise)
    /// </summary>
    /// <param name="zoom"></param>
    /// <param name="p"></param>
    public void SetZoomAndCenter(double zoom, Point p) => renderer.SetZoomAndCenter(zoom, p);

    public void ResetAll()
    {
        ResetSpeed();
        ResetRotation();
        ResetZoom();
    }

    public void ResetSpeed()
    {
        Speed = 1;
    }

    public void ResetRotation()
    {
        Rotation = 0;
    }

    public void ResetZoom()
    {
        bool npx = renderer.PanXOffset != 0;
        bool npy = renderer.PanYOffset != 0;
        bool npz = renderer.Zoom != 1;
        renderer.SetPanAll(0, 0, Rotation, 1, Renderer.ZoomCenterPoint, true); // Pan X/Y, Rotation, Zoom, Zoomcenter, Refresh

        UI(() =>
        {
            if (npx) Raise(nameof(PanXOffset));
            if (npy) Raise(nameof(PanYOffset));
            if (npz) Raise(nameof(Zoom));
        });
    }
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using FlyleafLib;

namespace LLPlayer.Controls;

public class SelectableTextBox : TextBox
{
    static SelectableTextBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectableTextBox), new FrameworkPropertyMetadata(typeof(SelectableTextBox)));
    }

    public static readonly DependencyProperty IsTranslatedProperty =
        DependencyProperty.Register(nameof(IsTranslated), typeof(bool), typeof(SelectableTextBox), new FrameworkPropertyMetadata(false));

    public bool IsTranslated
    {
        get => (bool)GetValue(IsTranslatedProperty);
        set => SetValue(IsTranslatedProperty, value);
    }

    public static readonly DependencyProperty SubIndexProperty =
        DependencyProperty.Register(nameof(SubIndex), typeof(int), typeof(SelectableTextBox), new FrameworkPropertyMetadata(0));

    public int SubIndex
    {
        get => (int)GetValue(SubIndexProperty);
        set => SetValue(SubIndexProperty, value);
    }

    public static readonly RoutedEvent WordClickedEvent =
        EventManager.RegisterRoutedEvent(nameof(WordClicked), RoutingStrategy.Bubble, typeof(WordClickedEventHandler), typeof(SelectableTextBox));

    public event WordClickedEventHandler WordClicked
    {
        add => AddHandler(WordClickedEvent, value);
        remove => RemoveHandler(WordClickedEvent, value);
    }

    private bool _isDragging;
    private int _dragStartIndex = -1;
    private int _dragEndIndex = -1;

    private Point _mouseDownPosition;

    protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnPreviewMouseLeftButtonDown(e);

        _isDragging = true;
        _mouseDownPosition = e.GetPosition(this);
        _dragStartIndex = GetCharacterIndexFromPoint(_mouseDownPosition, true);
        _dragEndIndex = _dragStartIndex;

        CaptureMouse();
        e.Handled = true;
    }

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        base.OnPreviewMouseMove(e);

        if (_isDragging)
        {
            Point currentPosition = e.GetPosition(this);

            if (currentPosition != _mouseDownPosition)
            {
                int currentIndex = GetCharacterIndexFromPoint(currentPosition, true);
                if (currentIndex != -1 && currentIndex != _dragEndIndex)
                {
                    _dragEndIndex = currentIndex;
                }
            }
        }
    }

    protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        base.OnPreviewMouseLeftButtonUp(e);

        if (_isDragging)
        {
            _isDragging = false;
            ReleaseMouseCapture();
            e.Handled = true;

            if (_dragStartIndex == _dragEndIndex)
            {
                // Click
                HandleClick(_mouseDownPosition, MouseClick.Left);
            }
            else
            {
                // Drag
                HandleDragSelection();
            }
        }
    }

    protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
    {
        base.OnMouseRightButtonUp(e);

        Point clickPosition = e.GetPosition(this);

        HandleClick(clickPosition, MouseClick.Right);

        // Right clicks other than words are currently disabled, consider creating another one
        e.Handled = true;
    }

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        // Middle click: Sentence Lookup
        if (e.ChangedButton == MouseButton.Middle)
        {
            if (string.IsNullOrEmpty(Text))
            {
                return;
            }

            WordClickedEventArgs args = new(WordClickedEvent)
            {
                Mouse = MouseClick.Middle,
                // Change line breaks to spaces to improve translation accuracy.
                Words = SubtitleTextUtil.FlattenText(Text),
                IsWord = false,
                Text = Text,
                IsTranslated = IsTranslated,
                SubIndex = SubIndex,
                WordOffset = 0,
                Sender = this
            };

            RaiseEvent(args);
        }
    }

    private void HandleClick(Point point, MouseClick mouse)
    {
        // false because it fires only above the word
        int charIndex = GetCharacterIndexFromPoint(point, false);

        if (charIndex == -1 || string.IsNullOrEmpty(Text))
        {
            return;
        }

        int start = FindWordStart(charIndex);
        int end = FindWordEnd(charIndex);

        // get word
        string word = Text.Substring(start, end - start).Trim();

        if (string.IsNullOrEmpty(word))
        {
            return;
        }

        WordClickedEventArgs args = new(WordClickedEvent)
        {
            Mouse = mouse,
            Words = word,
            IsWord = true,
            Text = Text,
            IsTranslated = IsTranslated,
            SubIndex = SubIndex,
            WordOffset = start,
            Sender = this
        };
        RaiseEvent(args);
    }

    private void HandleDragSelection()
    {
        // Support right to left drag
        int rawStart = Math.Min(_dragStartIndex, _dragEndIndex);
        int rawEnd = Math.Max(_dragStartIndex, _dragEndIndex);

        // Adjust to word boundaries
        int adjustedStart = FindWordStart(rawStart);
        int adjustedEnd = FindWordEnd(rawEnd);

        // Extract the substring within the adjusted selection
        // TODO: L: If there is only a delimiter after the end, I want to include it in the selection (should improve translation accuracy)
        string selectedText = Text.Substring(adjustedStart, adjustedEnd - adjustedStart);

        WordClickedEventArgs args = new(WordClickedEvent)
        {
            Mouse = MouseClick.Left,
            // Change line breaks to spaces to improve translation accuracy.
            Words = SubtitleTextUtil.FlattenText(selectedText),
            IsWord = false,
            Text = Text,
            IsTranslated = IsTranslated,
            SubIndex = SubIndex,
            WordOffset = adjustedStart,
            Sender = this
        };

        RaiseEvent(args);
    }

    private int FindWordStart(int index)
    {
        if (index < 0 || index > Text.Length)
        {
            return 0;
        }

        while (index > 0 && !IsWordSeparator(Text[index - 1]))
        {
            index--;
        }

        return index;
    }

    private int FindWordEnd(int index)
    {
        if (index < 0 || index > Text.Length)
        {
            return Text.Length;
        }

        while (index < Text.Length && !IsWordSeparator(Text[index]))
        {
            index++;
        }

        return index;
    }

    private static readonly HashSet<char> ExcludedSeparators = ['\'', '-'];

    private static bool IsWordSeparator(char c)
    {
        if (ExcludedSeparators.Contains(c))
        {
            return false;
        }

        return char.IsWhiteSpace(c) || char.IsPunctuation(c);
    }

    #region Cursor Hand
    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        // Change cursor only over word text
        int charIndex = GetCharacterIndexFromPoint(e.GetPosition(this), false);

        Cursor = charIndex != -1 ? Cursors.Hand : Cursors.Arrow;
    }

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);

        if (Cursor == Cursors.Hand)
        {
            Cursor = Cursors.Arrow;
        }
    }
    #endregion
}

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json.Serialization;
using Whisper.net;
using Whisper.net.LibraryLoader;

namespace FlyleafLib;

#nullable enable

// Whisper Common Config (whisper.cpp, faster-whisper)
public class WhisperConfig : NotifyPropertyChanged
{
    public static string ModelsDirectory { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "whispermodels");
    public static string EnginesDirectory { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Whisper");

    // For UI
    public string LanguageName
    {
        get
        {
            string translate = Translate ? ", Trans" : "";

            if (LanguageDetection)
            {
                return "Auto" + translate;
            }

            var lang = FlyleafLib.Language.Get(Language);

            if (lang != null)
            {
                return lang.TopEnglishName + translate;
            }

            return Language + translate;
        }
    }

    public string Language
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(LanguageName);
            }
        }
    } = "en";

    public bool LanguageDetection
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(LanguageName);
            }
        }
    } = true;

    public bool Translate
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(LanguageName);
            }
        }
    }

    public static void EnsureModelsDirectory()
    {
        if (!Directory.Exists(ModelsDirectory))
        {
            Directory.CreateDirectory(ModelsDirectory);
        }
    }

    public static void EnsureEnginesDirectory()
    {
        if (!Directory.Exists(EnginesDirectory))
        {
            Directory.CreateDirectory(EnginesDirectory);
        }
    }
}

// TODO: L: Add other options
public class WhisperCppConfig : NotifyPropertyChanged
{
    public WhisperCppModel? Model
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(IsEnglishModel));
            }
        }
    }

    [JsonIgnore]
    public bool IsEnglishModel => Model != null && Model.Model.ToString().EndsWith("En");

    public List<RuntimeLibrary> RuntimeLibraries { get; set => Set(ref field, value); } = [RuntimeLibrary.Cpu, RuntimeLibrary.CpuNoAvx];
    public RuntimeLibrary? LoadedLibrary => RuntimeOptions.LoadedLibrary;

    public int? GpuDevice { get; set => Set(ref field, value); }

    public int? Threads { get; set => Set(ref field, value); }
    public int? MaxSegmentLength { get; set => Set(ref field, value); }
    public int? MaxTokensPerSegment { get; set => Set(ref field, value); }
    public bool SplitOnWord { get; set => Set(ref field, value); }
    public float? NoSpeechThreshold { get; set => Set(ref field, value); }
    public bool NoContext { get; set => Set(ref field, value); }
    public int? AudioContextSize { get; set => Set(ref field, value); }
    public string Prompt { get; set => Set(ref field, value); } = string.Empty;

    public WhisperFactoryOptions GetFactoryOptions()
    {
        WhisperFactoryOptions opts = WhisperFactoryOptions.Default;

        if (GpuDevice.HasValue)
            opts.GpuDevice = GpuDevice.Value;

        return opts;
    }

    public WhisperProcessorBuilder ConfigureBuilder(WhisperConfig whisperConfig, WhisperProcessorBuilder builder)
    {
        if (IsEnglishModel)
        {
            // set English forcefully if English-only models
            builder.WithLanguage("en");
        }
        else
        {
            if (!string.IsNullOrEmpty(whisperConfig.Language))
                builder.WithLanguage(whisperConfig.Language);

            // prefer auto
            if (whisperConfig.LanguageDetection)
                builder.WithLanguageDetection();

            if (whisperConfig.Translate)
                builder.WithTranslate();
        }

        if (Threads is > 0)
            builder.WithThreads(Threads.Value);

        if (MaxSegmentLength is > 0)
            builder.WithMaxSegmentLength(MaxSegmentLength.Value);

        if (MaxTokensPerSegment is > 0)
            builder.WithMaxTokensPerSegment(MaxTokensPerSegment.Value);

        if (SplitOnWord)
            builder.SplitOnWord();

        if (NoSpeechThreshold is > 0)
            builder.WithNoSpeechThreshold(NoSpeechThreshold.Value);

        if (NoContext)
            builder.WithNoContext();

        if (AudioContextSize is > 0)
            builder.WithAudioContextSize(AudioContextSize.Value);

        if (!string.IsNullOrWhiteSpace(Prompt))
            builder.WithPrompt(Prompt);

        // auto set
        if (MaxSegmentLength is > 0 || MaxSegmentLength is > 0)
            builder.WithTokenTimestamps();

        return builder;
    }
}

public class FasterWhisperConfig : NotifyPropertyChanged
{
    public static string DefaultEnginePath { get; } = Path.Combine(WhisperConfig.EnginesDirectory, "Faster-Whisper-XXL", "faster-whisper-xxl.exe");

    // can get by faster-whisper-xxl.exe --model foo bar.wav
    public static List<string> ModelOptions { get; } = [
        "tiny",
        "tiny.en",
        "base",
        "base.en",
        "small",
        "small.en",
        "medium",
        "medium.en",
        "large-v1",
        "large-v2",
        "large-v3",
        //"large", // = large-v3
        "large-v3-turbo",
        //"turbo", // = large-v3-turbo
        "distil-large-v2",
        "distil-medium.en",
        "distil-small.en",
        "distil-large-v3",
        "distil-large-v3.5"
    ];

    public bool UseManualEngine { get; set => Set(ref field, value); }
    public string? ManualEnginePath { get; set => Set(ref field, value); }
    public bool UseManualModel { get; set => Set(ref field, value); }
    public string? ManualModelDir { get; set => Set(ref field, value); }
    public string Model
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(IsEnglishModel));
            }
        }
    } = "tiny";
    [JsonIgnore]
    public bool IsEnglishModel => Model.EndsWith(".en") ||
                                  Model is "distil-large-v2"
                                        or "distil-large-v3"
                                        or "distil-large-v3.5";
    public string ExtraArguments { get; set => Set(ref field, value); } = string.Empty;

    public ProcessPriorityClass ProcessPriority { get; set => Set(ref field, value); } = ProcessPriorityClass.Normal;
}

using System.Collections.Generic;
using System.Diagnostics;

namespace FlyleafLib.MediaFramework.MediaRemuxer;

public unsafe class Remuxer
{
    public int                  UniqueId            { get; set; }
    public bool                 Disposed            { get; private set; } = true;
    public string               Filename            { get; private set; }
    public bool                 HasStreams          => mapInOutStreams2.Count > 0 || mapInOutStreams.Count > 0;
    public bool                 HeaderWritten       { get; private set; }

    Dictionary<IntPtr, IntPtr>  mapInOutStreams     = new();
    Dictionary<int, IntPtr>     mapInInStream       = new();
    Dictionary<int, long>       mapInStreamToDts    = new();
    Dictionary<IntPtr, IntPtr>  mapInOutStreams2    = new();
    Dictionary<int, IntPtr>     mapInInStream2      = new();
    Dictionary<int, long>       mapInStreamToDts2   = new();

    AVFormatContext* fmtCtx;
    AVOutputFormat* fmt;

    public Remuxer(int uniqueId = -1)
        => UniqueId = uniqueId == -1 ? Utils.GetUniqueId() : uniqueId;

    public int Open(string filename)
    {
        int ret;
        Filename = filename;

        fixed (AVFormatContext** ptr = &fmtCtx)
            ret = avformat_alloc_output_context2(ptr, null, null, Filename);

        if (ret < 0) return ret;

        fmt = fmtCtx->oformat;
        mapInStreamToDts = new Dictionary<int, long>();
        Disposed = false;

        return 0;
    }

    public int AddStream(AVStream* in_stream, bool isAudioDemuxer = false)
    {
        int ret = -1;

        if (in_stream == null || (in_stream->codecpar->codec_type != AVMediaType.Video && in_stream->codecpar->codec_type != AVMediaType.Audio)) return ret;

        AVStream *out_stream;
        var in_codecpar = in_stream->codecpar;

        out_stream = avformat_new_stream(fmtCtx, null);
        if (out_stream == null) return -1;

        ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
        if (ret < 0) return ret;

        // Copy metadata (currently only language)
        AVDictionaryEntry* b = null;
        while (true)
        {
            b = av_dict_get(in_stream->metadata, "", b, DictReadFlags.IgnoreSuffix);
            if (b == null) break;

            if (Utils.BytePtrToStringUTF8(b->key).ToLower() == "language" || Utils.BytePtrToStringUTF8(b->key).ToLower() == "lang")
                av_dict_set(&out_stream->metadata, Utils.BytePtrToStringUTF8(b->key), Utils.BytePtrToStringUTF8(b->value), 0);
        }

        out_stream->codecpar->codec_tag = 0;

        // TODO:
        // validate that the output format container supports the codecs (if not change the container?)
        // check whether we remux from mp4 to mpegts (requires bitstream filter h264_mp4toannexb)

        //if (av_codec_get_tag(fmtCtx->oformat->codec_tag, in_stream->codecpar->codec_id) == 0)
        //    Log("Not Found");
        //else
        //    Log("Found");

        if (isAudioDemuxer)
        {
            mapInOutStreams2.Add((IntPtr)in_stream, (IntPtr)out_stream);
            mapInInStream2.Add(in_stream->index, (IntPtr)in_stream);
        }
        else
        {
            mapInOutStreams.Add((IntPtr)in_stream, (IntPtr)out_stream);
            mapInInStream.Add(in_stream->index, (IntPtr)in_stream);
        }

        return 0;
    }

    public int WriteHeader()
    {
        if (!HasStreams) throw new Exception("No streams have been configured for the remuxer");

        int ret;

        ret = avio_open(&fmtCtx->pb, Filename, IOFlags.Write);
        if (ret < 0) { Dispose(); return ret; }

        ret = avformat_write_header(fmtCtx, null);

        if (ret < 0) { Dispose(); return ret; }

        HeaderWritten = true;

        return 0;
    }

    public int Write(AVPacket* packet, bool isAudioDemuxer = false)
    {
        lock (this)
        {
            var mapInInStream       = !isAudioDemuxer? this.mapInInStream   : mapInInStream2;
            var mapInOutStreams     = !isAudioDemuxer? this.mapInOutStreams : mapInOutStreams2;
            var mapInStreamToDts    = !isAudioDemuxer? this.mapInStreamToDts: mapInStreamToDts2;

            AVStream* in_stream     =  (AVStream*) mapInInStream[packet->stream_index];
            AVStream* out_stream    =  (AVStream*) mapInOutStreams[(IntPtr)in_stream];

            if (packet->dts != AV_NOPTS_VALUE)
            {

                if (!mapInStreamToDts.ContainsKey(in_stream->index))
                {
                    // TODO: In case of AudioDemuxer calculate the diff with the VideoDemuxer and add it in one of them - all stream - (in a way to have positive)
                    mapInStreamToDts.Add(in_stream->index, packet->dts);
                }

                packet->pts = packet->pts == AV_NOPTS_VALUE ? AV_NOPTS_VALUE : av_rescale_q_rnd(packet->pts - mapInStreamToDts[in_stream->index], in_stream->time_base, out_stream->time_base, AVRounding.NearInf | AVRounding.PassMinmax);
                packet->dts = av_rescale_q_rnd(packet->dts - mapInStreamToDts[in_stream->index], in_stream->time_base, out_stream->time_base, AVRounding.NearInf | AVRounding.PassMinmax);
            }
            else
            {
                packet->pts = packet->pts == AV_NOPTS_VALUE ? AV_NOPTS_VALUE : av_rescale_q_rnd(packet->pts, in_stream->time_base, out_stream->time_base, AVRounding.NearInf | AVRounding.PassMinmax);
                packet->dts = AV_NOPTS_VALUE;
            }

            packet->duration        = av_rescale_q(packet->duration,in_stream->time_base, out_stream->time_base);
            packet->stream_index    = out_stream->index;
            packet->pos             = -1;

            int ret = av_interleaved_write_frame(fmtCtx, packet);
            av_packet_free(&packet);

            return ret;
        }
    }

    public int WriteTrailer() => Dispose();
    public int Dispose()
    {
        if (Disposed) return -1;

        int ret = 0;

        if (fmtCtx != null)
        {
            if (HeaderWritten)
            {
                ret = av_write_trailer(fmtCtx);
                avio_closep(&fmtCtx->pb);
            }

            avformat_free_context(fmtCtx);
        }

        fmtCtx = null;
        Filename = null;
        Disposed = true;
        HeaderWritten = false;
        mapInOutStreams.Clear();
        mapInInStream.Clear();
        mapInOutStreams2.Clear();
        mapInInStream2.Clear();

        return ret;
    }

    private void Log(string msg)
        => Debug.WriteLine($"[{DateTime.Now:hh.mm.ss.fff}] [#{UniqueId}] [Remuxer] {msg}");
}

using System.Windows;

namespace LLPlayer.Extensions;

/// <summary>
/// Customize DialogService's Show(), which sets the Owner of the Window and sets it to always-on-top.
/// ref: https://stackoverflow.com/questions/64420093/prism-idialogservice-show-non-modal-dialog-acts-as-modal
/// </summary>
public class ExtendedDialogService(IContainerExtension containerExtension) : DialogService(containerExtension)
{
    private bool _isOrphan;

    protected override void ConfigureDialogWindowContent(string dialogName, IDialogWindow window, IDialogParameters parameters)
    {
        base.ConfigureDialogWindowContent(dialogName, window, parameters);

        if (parameters != null &&
            parameters.ContainsKey(MyKnownDialogParameters.OrphanWindow))
        {
            _isOrphan = true;
        }
    }

    protected override void ShowDialogWindow(IDialogWindow dialogWindow, bool isModal)
    {
        base.ShowDialogWindow(dialogWindow, isModal);

        if (_isOrphan)
        {
            // Show and then clear Owner to place the window based on the parent window and then make it an orphan window.
            _isOrphan = false;
            dialogWindow.Owner = null;
        }
    }
}

// ref: https://github.com/PrismLibrary/Prism/blob/master/src/Wpf/Prism.Wpf/Dialogs/IDialogServiceCompatExtensions.cs
public static class ExtendedDialogServiceExtensions
{
    /// <summary>
    /// Shows a non-modal and singleton dialog.
    /// </summary>
    /// <param name="dialogService">The DialogService</param>
    /// <param name="name">The name of the dialog to show.</param>
    /// <param name="orphan">Whether to set owner to window</param>
    public static void ShowSingleton(this IDialogService dialogService, string name, bool orphan)
    {
        ShowSingleton(dialogService, name, null!, orphan);
    }

    /// <summary>
    /// Shows a non-modal and singleton dialog.
    /// </summary>
    /// <param name="dialogService">The DialogService</param>
    /// <param name="name">The name of the dialog to show.</param>
    /// <param name="callback">The action to perform when the dialog is closed.</param>
    /// <param name="orphan">Whether to set owner to window</param>
    public static void ShowSingleton(this IDialogService dialogService, string name, Action<IDialogResult> callback, bool orphan)
    {
        var parameters = EnsureShowNonModalParameter(null);

        var windows = Application.Current.Windows.OfType<IDialogWindow>();
        if (windows.Any())
        {
            var curWindow = windows.FirstOrDefault(w => w.Content.GetType().Name == name);
            if (curWindow != null && curWindow is Window win)
            {
                // If minimized, it will not be displayed after Activate, so set it back to Normal in advance.
                if (win.WindowState == WindowState.Minimized)
                {
                    win.WindowState = WindowState.Normal;
                }
                // TODO: L: Notify to ViewModel to update query
                win.Activate();
                return;
            }
        }

        if (orphan)
        {
            parameters.Add(MyKnownDialogParameters.OrphanWindow, true);
        }

        dialogService.Show(name, parameters, callback);
    }

    private static IDialogParameters EnsureShowNonModalParameter(IDialogParameters? parameters)
    {
        parameters ??= new DialogParameters();

        if (!parameters.ContainsKey(KnownDialogParameters.ShowNonModal))
            parameters.Add(KnownDialogParameters.ShowNonModal, true);

        return parameters;
    }
}

public static class MyKnownDialogParameters
{
    public const string OrphanWindow = "orphanWindow";
}

using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using LLPlayer.Extensions;

namespace LLPlayer.Controls.Settings;

public partial class SettingsAbout : UserControl
{
    public SettingsAbout()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsAboutVM>();
    }
}

public class SettingsAboutVM : Bindable
{
    public SettingsAboutVM()
    {
        Libraries =
        [
            new LibraryInfo
            {
                Name = "SuRGeoNix/Flyleaf",
                Description = "Media Player .NET Library for WinUI 3/WPF/WinForms (based on FFmpeg/DirectX)",
                Url = "https://github.com/SuRGeoNix/Flyleaf"
            },

            new LibraryInfo
            {
                Name = "SuRGeoNix/Flyleaf.FFmpeg",
                Description = "FFmpeg Bindings for C#/.NET",
                Url = "https://github.com/SuRGeoNix/Flyleaf.FFmpeg"
            },

            new LibraryInfo
            {
                Name = "sandrohanea/whisper.net",
                Description = "Dotnet bindings for OpenAI Whisper (whisper.cpp)",
                Url = "https://github.com/sandrohanea/whisper.net"
            },

            new LibraryInfo
            {
                Name = "Purfview/whisper-standalone-win",
                Description = "Faster-Whisper standalone executables",
                Url = "https://github.com/Purfview/whisper-standalone-win"
            },

            new LibraryInfo
            {
                Name = "Sicos1977/TesseractOCR",
                Description = ".NET wrapper for Tesseract OCR",
                Url = "https://github.com/Sicos1977/TesseractOCR"
            },

            new LibraryInfo
            {
                Name = "MaterialDesignInXamlToolkit ",
                Description = "Google's Material Design in XAML & WPF",
                Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit"
            },

            new LibraryInfo
            {
                Name = "searchpioneer/lingua-dotnet",
                Description = "Natural language detection library for .NET",
                Url = "https://github.com/searchpioneer/lingua-dotnet"
            },

            new LibraryInfo
            {
                Name = "CharsetDetector/UTF-unknowns",
                Description = "Character set detector build in C#",
                Url = "https://github.com/CharsetDetector/UTF-unknown"
            },

            new LibraryInfo
            {
                Name = "komutan/NMeCab",
                Description = "Japanese morphological analyzer on .NET",
                Url = "https://github.com/komutan/NMeCab"
            },

            new LibraryInfo
            {
                Name = "PrismLibrary/Prism",
                Description = "A framework for building MVVM application",
                Url = "https://github.com/PrismLibrary/Prism"
            },

            new LibraryInfo
            {
                Name = "amerkoleci/Vortice.Windows",
                Description = ".NET bindings for Direct3D11, XAudio, etc.",
                Url = "https://github.com/amerkoleci/Vortice.Windows"
            },

            new LibraryInfo
            {
                Name = "sskodje/WpfColorFont",
                Description = " A WPF font and color dialog",
                Url = "https://github.com/sskodje/WpfColorFont"
            },
        ];

    }
    public ObservableCollection<LibraryInfo> Libraries { get; }

    [field: AllowNull, MaybeNull]
    public DelegateCommand CmdCopyVersion => field ??= new(() =>
    {
        Clipboard.SetText($"""
                           Version: {App.Version}, CommitHash: {App.CommitHash}
                           OS Architecture: {App.OSArchitecture}, Process Architecture: {App.ProcessArchitecture}
                           """);
    });
}

public class LibraryInfo
{
    public required string Name { get; init; }
    public required string Description { get; init; }
    public required string Url { get; init; }
}

using System.ComponentModel;
using System.Windows.Data;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.ViewModels;

public class SubtitlesSidebarVM : Bindable, IDisposable
{
    public FlyleafManager FL { get; }

    public SubtitlesSidebarVM(FlyleafManager fl)
    {
        FL = fl;

        FL.Config.PropertyChanged += OnConfigOnPropertyChanged;

        // Initialize filtered view for the sidebar
        for (int i = 0; i < _filteredSubs.Length; i++)
        {
            _filteredSubs[i] = (ListCollectionView)CollectionViewSource.GetDefaultView(FL.Player.SubtitlesManager[i].Subs);
        }
    }

    public void Dispose()
    {
        FL.Config.PropertyChanged -= OnConfigOnPropertyChanged;
    }

    private void OnConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs args)
    {
        switch (args.PropertyName)
        {
            case nameof(FL.Config.SidebarShowSecondary):
                OnPropertyChanged(nameof(SubIndex));
                OnPropertyChanged(nameof(SubManager));

                // prevent unnecessary filter update
                if (_lastSearchText[SubIndex] != _trimSearchText)
                {
                    ApplyFilter(); // ensure filter applied
                }
                break;
            case nameof(FL.Config.SidebarShowOriginalText):
                // Update ListBox
                OnPropertyChanged(nameof(SubManager));

                if (_trimSearchText.Length != 0)
                {
                    // because of switch between Text and DisplayText
                    ApplyFilter();
                }
                break;
        }
    }

    public int SubIndex => !FL.Config.SidebarShowSecondary ? 0 : 1;

    public SubManager SubManager => FL.Player.SubtitlesManager[SubIndex];

    private readonly ListCollectionView[] _filteredSubs = new ListCollectionView[2];

    private string _searchText = string.Empty;
    public string SearchText
    {
        get => _searchText;
        set
        {
            if (Set(ref _searchText, value))
            {
                DebounceFilter();
            }
        }
    }

    private string _trimSearchText = string.Empty; // for performance

    public string HitCount { get; set => Set(ref field, value); } = string.Empty;

    public event EventHandler? RequestScrollToTop;

    // TODO: L: Fix implicit changes to reflect
    public DelegateCommand<string>? CmdSubFontSizeChange => field ??= new(increase =>
    {
        if (int.TryParse(increase, out var value))
        {
            FL.Config.SidebarFontSize += value;
        }
    });

    public DelegateCommand? CmdSubTextMaskToggle => field ??= new(() =>
    {
        FL.Config.SidebarTextMask = !FL.Config.SidebarTextMask;

        // Update ListBox
        OnPropertyChanged(nameof(SubManager));
    });

    public DelegateCommand? CmdShowDownloadSubsDialog => field ??= new(() =>
    {
        FL.Action.CmdOpenWindowSubsDownloader.Execute();
    });

    public DelegateCommand? CmdShowExportSubsDialog => field ??= new(() =>
    {
        FL.Action.CmdOpenWindowSubsExporter.Execute();
    });

    public DelegateCommand? CmdSwapSidebarPosition => field ??= new(() =>
    {
        FL.Config.SidebarLeft = !FL.Config.SidebarLeft;
    });

    public DelegateCommand<int?>? CmdSubPlay => field ??= new((index) =>
    {
        if (!index.HasValue)
        {
            return;
        }

        var sub = SubManager.Subs[index.Value];
        FL.Player.SeekAccurate(sub.StartTime, SubIndex);
    });

    public DelegateCommand<int?>? CmdSubSync => field ??= new((index) =>
    {
        if (!index.HasValue)
        {
            return;
        }

        var sub = SubManager.Subs[index.Value];
        var newDelay = FL.Player.CurTime - sub.StartTime.Ticks;

        // Delay's OSD is not displayed, temporarily set to Active
        FL.Player.Activity.ForceActive();

        FL.PlayerConfig.Subtitles[SubIndex].Delay = newDelay;
    });

    public DelegateCommand? CmdClearSearch => field ??= new(() =>
    {
        if (!FL.Config.SidebarSearchActive)
            return;

        Set(ref _searchText, string.Empty, nameof(SearchText));
        _trimSearchText = string.Empty;
        HitCount = string.Empty;

        _debounceCts?.Cancel();
        _debounceCts?.Dispose();
        _debounceCts = null;

        for (int i = 0; i < _filteredSubs.Length; i++)
        {
            _lastSearchText[i] = string.Empty;
            _filteredSubs[i].Filter = null; // remove filter completely
            _filteredSubs[i].Refresh();
        }

        FL.Config.SidebarSearchActive = false;

        // move focus to video and enable keybindings
        FL.FlyleafHost!.Surface.Focus();

        // for scrolling to current sub
        var prev = SubManager.SelectedSub;
        SubManager.SelectedSub = null;
        SubManager.SelectedSub = prev;
    });

    public DelegateCommand? CmdNextMatch => field ??= new(() =>
    {
        if (_filteredSubs[SubIndex].IsEmpty)
            return;

        if (!_filteredSubs[SubIndex].MoveCurrentToNext())
        {
            // if last, move to first
            _filteredSubs[SubIndex].MoveCurrentToFirst();
        }

        var nextItem = (SubtitleData)_filteredSubs[SubIndex].CurrentItem;
        if (nextItem != null)
        {
            FL.Player.SeekAccurate(nextItem.StartTime, SubIndex);
            FL.Player.Activity.RefreshFullActive();
        }
    });

    public DelegateCommand? CmdPrevMatch => field ??= new(() =>
    {
        if (_filteredSubs[SubIndex].IsEmpty)
            return;

        if (!_filteredSubs[SubIndex].MoveCurrentToPrevious())
        {
            // if first, move to last
            _filteredSubs[SubIndex].MoveCurrentToLast();
        }

        var prevItem = (SubtitleData)_filteredSubs[SubIndex].CurrentItem;
        if (prevItem != null)
        {
            FL.Player.SeekAccurate(prevItem.StartTime, SubIndex);
            FL.Player.Activity.RefreshFullActive();
        }
    });

    // Debounce logic
    private CancellationTokenSource? _debounceCts;
    private async void DebounceFilter()
    {
        _debounceCts?.Cancel();
        _debounceCts?.Dispose();
        _debounceCts = new CancellationTokenSource();
        var token = _debounceCts.Token;

        try
        {
            await Task.Delay(300, token); // 300ms debounce

            if (!token.IsCancellationRequested)
            {
                ApplyFilter();
            }
        }
        catch (OperationCanceledException)
        {
            // ignore
        }
    }

    private readonly string[] _lastSearchText = [string.Empty, string.Empty];

    private void ApplyFilter()
    {
        _trimSearchText = SearchText.Trim();
        _lastSearchText[SubIndex] = _trimSearchText;

        // initialize filter lazily
        _filteredSubs[SubIndex].Filter ??= SubFilter;
        _filteredSubs[SubIndex].Refresh();

        int count = _filteredSubs[SubIndex].Count;
        HitCount = count > 0 ? $"{count} hits" : "No hits";

        if (SubManager.SelectedSub != null && _filteredSubs[SubIndex].MoveCurrentTo(SubManager.SelectedSub))
        {
            // scroll to current playing item
            var prev = SubManager.SelectedSub;
            SubManager.SelectedSub = null;
            SubManager.SelectedSub = prev;
        }
        else
        {
            // scroll to top
            if (!_filteredSubs[SubIndex].IsEmpty)
            {
                RequestScrollToTop?.Invoke(this, EventArgs.Empty);
            }
        }
    }

    private bool SubFilter(object obj)
    {
        if (_trimSearchText.Length == 0) return true;
        if (obj is not SubtitleData sub) return false;

        string? source = FL.Config.SidebarShowOriginalText ? sub.Text : sub.DisplayText;
        return source?.IndexOf(_trimSearchText, StringComparison.OrdinalIgnoreCase) >= 0;
    }
}

using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace FlyleafLib;

public static class Logger
{
    public static bool CanError => Engine.Config.LogLevel >= LogLevel.Error;
    public static bool CanWarn  => Engine.Config.LogLevel >= LogLevel.Warn;
    public static bool CanInfo  => Engine.Config.LogLevel >= LogLevel.Info;
    public static bool CanDebug => Engine.Config.LogLevel >= LogLevel.Debug;
    public static bool CanTrace => Engine.Config.LogLevel >= LogLevel.Trace;

    public static Action<string>
                        CustomOutput = DevNullPtr;
    internal static Action<string>
                        Output = DevNullPtr;
    static string       lastOutput = "";

    static ConcurrentQueue<byte[]>
                        fileData = new();
    static bool         fileTaskRunning;
    static FileStream   fileStream;
    static object       lockFileStream = new();
    static Dictionary<LogLevel, string>
                        logLevels = new();

    static Logger()
    {
        foreach (LogLevel loglevel in Enum.GetValues(typeof(LogLevel)))
            logLevels.Add(loglevel, loglevel.ToString().PadRight(5, ' '));

        // Flush File Data on Application Exit
        System.Windows.Application.Current.Exit += (o, e) =>
        {
            lock (lockFileStream)
            {
                if (fileStream != null)
                {
                    while (fileData.TryDequeue(out byte[] data))
                        fileStream.Write(data, 0, data.Length);
                    fileStream.Dispose();
                }
            }
        };
    }

    internal static void SetOutput()
    {
        string output = Engine.Config.LogOutput;

        if (string.IsNullOrEmpty(output))
        {
            if (lastOutput != "")
            {
                Output = DevNullPtr;
                lastOutput = "";
            }
        }
        else if (output.StartsWith(":"))
        {
            if (output == ":console")
            {
                if (lastOutput != ":console")
                {
                    Output = Console.WriteLine;
                    lastOutput = ":console";
                }
            }
            else if (output == ":debug")
            {
                if (lastOutput != ":debug")
                {
                    Output = DebugPtr;
                    lastOutput = ":debug";
                }
            }
            else if (output == ":custom")
            {
                if (lastOutput != ":custom")
                {
                    Output = CustomOutput;
                    lastOutput = ":custom";
                }
            }
            else
                throw new Exception("Invalid log output");
        }
        else
        {
            lock (lockFileStream)
            {
                // Flush File Data on Previously Opened File Stream
                if (fileStream != null)
                {
                    while (fileData.TryDequeue(out byte[] data))
                        fileStream.Write(data, 0, data.Length);
                    fileStream.Dispose();
                }

                string dir = Path.GetDirectoryName(output);
                if (!string.IsNullOrEmpty(dir))
                    Directory.CreateDirectory(dir);

                fileStream = new FileStream(output, Engine.Config.LogAppend ? FileMode.Append : FileMode.Create, FileAccess.Write);
                if (lastOutput != ":file")
                {
                    Output = FilePtr;
                    lastOutput = ":file";
                }
            }
        }
    }
    static void DebugPtr(string msg) => System.Diagnostics.Debug.WriteLine(msg);
    static void DevNullPtr(string msg) { }
    static void FilePtr(string msg)
    {
        fileData.Enqueue(Encoding.UTF8.GetBytes($"{msg}\r\n"));

        if (!fileTaskRunning && fileData.Count > Engine.Config.LogCachedLines)
            FlushFileData();
    }

    static void FlushFileData()
    {
        fileTaskRunning = true;

        Task.Run(() =>
        {
            lock (lockFileStream)
            {
                while (fileData.TryDequeue(out byte[] data))
                    fileStream.Write(data, 0, data.Length);

                fileStream.Flush();
            }

            fileTaskRunning = false;
        });
    }

    /// <summary>
    /// Forces cached file data to be written to the file
    /// </summary>
    public static void ForceFlush()
    {
        if (!fileTaskRunning && fileStream != null)
            FlushFileData();
    }

    internal static void Log(string msg, LogLevel logLevel)
    {
        if (logLevel <= Engine.Config.LogLevel)
            Output($"{DateTime.Now.ToString(Engine.Config.LogDateTimeFormat)} | {logLevels[logLevel]} | {msg}");
    }
}

public class LogHandler
{
    public string Prefix;

    public LogHandler(string prefix = "")
        => Prefix = prefix;

    public void Error(string msg)   => Logger.Log($"{Prefix}{msg}", LogLevel.Error);
    public void Info(string msg)    => Logger.Log($"{Prefix}{msg}", LogLevel.Info);
    public void Warn(string msg)    => Logger.Log($"{Prefix}{msg}", LogLevel.Warn);
    public void Debug(string msg)   => Logger.Log($"{Prefix}{msg}", LogLevel.Debug);
    public void Trace(string msg)   => Logger.Log($"{Prefix}{msg}", LogLevel.Trace);
}

public enum LogLevel
{
    Quiet   = 0x00,
    Error   = 0x10,
    Warn    = 0x20,
    Info    = 0x30,
    Debug   = 0x40,
    Trace   = 0x50
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;

namespace LLPlayer.Extensions;

/// <summary>
/// Behavior to disable scrolling in ListView
/// ref: https://stackoverflow.com/questions/1585462/bubbling-scroll-events-from-a-listview-to-its-parent
/// </summary>
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
        base.OnDetaching();
    }

    private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        var scrollViewer = GetVisualChild<ScrollViewer>(AssociatedObject);
        var scrollPos = scrollViewer!.ContentVerticalOffset;
        if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
            || (scrollPos == 0 && e.Delta > 0))
        {
            e.Handled = true;
            MouseWheelEventArgs e2 = new(e.MouseDevice, e.Timestamp, e.Delta);
            e2.RoutedEvent = UIElement.MouseWheelEvent;
            AssociatedObject.RaiseEvent(e2);
        }
    }

    private static T? GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T? child = null;

        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }

            if (child != null)
            {
                break;
            }
        }

        return child;
    }
}

using System.IO;
using System.Text;
using UtfUnknown;

namespace FlyleafLib;

#nullable enable

public static class TextEncodings
{
    private static Encoding? DetectEncodingInternal(byte[] data)
    {
        // 1. Check Unicode BOM
        Encoding? encoding = DetectEncodingWithBOM(data);
        if (encoding != null)
        {
            return encoding;
        }

        // 2. If no BOM, then check text is UTF-8 without BOM
        // Perform UTF-8 check first because automatic detection often results in false positives such as WINDOWS-1252.
        if (IsUtf8(data))
        {
            return Encoding.UTF8;
        }

        // 3. Auto detect encoding using library
        try
        {
            var result = CharsetDetector.DetectFromBytes(data);
            return result.Detected.Encoding;
        }
        catch
        {
            return null;
        }
    }

    /// <summary>
    /// Detect character encoding of text binary files
    /// </summary>
    /// <param name="original">text binary</param>
    /// <param name="maxBytes">Bytes to read</param>
    /// <returns>Detected Encoding or null</returns>
    public static Encoding? DetectEncoding(byte[] original, int maxBytes = 1 * 1024 * 1024)
    {
        if (maxBytes > original.Length)
        {
            maxBytes = original.Length;
        }

        byte[] data = new byte[maxBytes];
        Array.Copy(original, data, maxBytes);

        return DetectEncodingInternal(data);
    }

    /// <summary>
    /// Detect character encoding of text files
    /// </summary>
    /// <param name="path">file path</param>
    /// <param name="maxBytes">Bytes to read</param>
    /// <returns>Detected Encoding or null</returns>
    public static Encoding? DetectEncoding(string path, int maxBytes = 1 * 1024 * 1024)
    {
        byte[] data = new byte[maxBytes];

        try
        {
            using FileStream fs = new(path, FileMode.Open, FileAccess.Read);
            int bytesRead = fs.Read(data, 0, data.Length);
            Array.Resize(ref data, bytesRead);
        }
        catch
        {
            return null;
        }

        return DetectEncodingInternal(data);
    }

    /// <summary>
    /// Detect character encoding using BOM
    /// </summary>
    /// <param name="bytes">string raw data</param>
    /// <returns>Detected Encoding or null</returns>
    private static Encoding? DetectEncodingWithBOM(byte[] bytes)
    {
        // UTF-8 BOM: EF BB BF
        if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
        {
            return Encoding.UTF8;
        }

        // UTF-16 LE BOM: FF FE
        if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
        {
            return Encoding.Unicode; // UTF-16 LE
        }

        // UTF-16 BE BOM: FE FF
        if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
        {
            return Encoding.BigEndianUnicode; // UTF-16 BE
        }

        // UTF-32 LE BOM: FF FE 00 00
        if (bytes.Length >= 4 && bytes[0] == 0xFF && bytes[1] == 0xFE && bytes[2] == 0x00 && bytes[3] == 0x00)
        {
            return Encoding.UTF32;
        }

        // UTF-32 BE BOM: 00 00 FE FF
        if (bytes.Length >= 4 && bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF)
        {
            return new UTF32Encoding(bigEndian: true, byteOrderMark: true);
        }

        // No BOM
        return null;
    }

    private static bool IsUtf8(byte[] bytes)
    {
        // enable validation
        UTF8Encoding encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

        try
        {
            encoding.GetString(bytes);

            return true;
        }
        catch (DecoderFallbackException ex)
        {
            // Ignore when a trailing cut character causes a validation error.
            if (bytes.Length - ex.Index < 4)
            {
                return true;
            }

            return false;
        }
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Windows.Input;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer;

partial class Player
{
    /* Player Key Bindings
     *
     * Config.Player.KeyBindings.Keys
     *
     * KeyDown / KeyUp Events (Control / WinFormsHost / WindowFront (FlyleafWindow))
     * Exposes KeyDown/KeyUp if required to listen on additional Controls/Windows
     * Allows KeyBindingAction.Custom to set an external Action for Key Binding
     */

    Tuple<KeyBinding,long> onKeyUpBinding;

    /// <summary>
    /// Can be used to route KeyDown events (WPF)
    /// </summary>
    /// <param name="player"></param>
    /// <param name="e"></param>
    public static bool KeyDown(Player player, KeyEventArgs e)
    {
        e.Handled = KeyDown(player, e.Key == Key.System ? e.SystemKey : e.Key);

        return e.Handled;
    }

    /// <summary>
    /// Can be used to route KeyDown events (WinForms)
    /// </summary>
    /// <param name="player"></param>
    /// <param name="e"></param>
    public static void KeyDown(Player player, System.Windows.Forms.KeyEventArgs e)
        => e.Handled = KeyDown(player, KeyInterop.KeyFromVirtualKey((int)e.KeyCode));

    /// <summary>
    /// Can be used to route KeyUp events (WPF)
    /// </summary>
    /// <param name="player"></param>
    /// <param name="e"></param>
    public static bool KeyUp(Player player, KeyEventArgs e)
    {
        e.Handled = KeyUp(player, e.Key == Key.System ? e.SystemKey : e.Key);

        return e.Handled;
    }

    /// <summary>
    /// Can be used to route KeyUp events (WinForms)
    /// </summary>
    /// <param name="player"></param>
    /// <param name="e"></param>
    public static void KeyUp(Player player, System.Windows.Forms.KeyEventArgs e)
        => e.Handled = KeyUp(player, KeyInterop.KeyFromVirtualKey((int)e.KeyCode));

    public static bool KeyDown(Player player, Key key)
    {
        if (player == null)
            return false;

        player.Activity.RefreshActive();

        if (player.onKeyUpBinding != null)
        {
            if (player.onKeyUpBinding.Item1.Key == key)
                return true;

            if (DateTime.UtcNow.Ticks - player.onKeyUpBinding.Item2 < TimeSpan.FromSeconds(2).Ticks)
                return false;

            player.onKeyUpBinding = null; // In case of keyboard lost capture (should be handled from hosts)
        }

        List<KeyBinding> keysList = new();
        var spanList = CollectionsMarshal.AsSpan(player.Config.Player.KeyBindings.Keys); // should create dictionary here with key+alt+ctrl+shift hash
        foreach(var binding in spanList)
            if (binding.Key == key && binding.IsEnabled)
                keysList.Add(binding);

        if (keysList.Count == 0)
            return false;

        bool alt, ctrl, shift;
        alt     = Keyboard.IsKeyDown(Key.LeftAlt)   || Keyboard.IsKeyDown(Key.RightAlt);
        ctrl    = Keyboard.IsKeyDown(Key.LeftCtrl)  || Keyboard.IsKeyDown(Key.RightCtrl);
        shift   = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);

        var spanList2 = CollectionsMarshal.AsSpan(keysList);
        foreach(var binding in spanList2)
        {
            if (binding.Alt == alt && binding.Ctrl == ctrl && binding.Shift == shift && binding.IsEnabled)
            {
                if (binding.IsKeyUp)
                    player.onKeyUpBinding = new(binding, DateTime.UtcNow.Ticks);
                else
                    ExecuteBinding(player, binding, false);

                return true;
            }
        }

        return false;
    }
    public static bool KeyUp(Player player, Key key)
    {
        if (player == null || player.onKeyUpBinding == null || player.onKeyUpBinding.Item1.Key != key)
            return false;

        ExecuteBinding(player, player.onKeyUpBinding.Item1, true);
        player.onKeyUpBinding = null;
        return true;
    }

    static void ExecuteBinding(Player player, KeyBinding binding, bool isKeyUp)
    {
        if (CanDebug) player.Log.Debug($"[Keys|{(isKeyUp ? "Up" : "Down")}] {(binding.Action == KeyBindingAction.Custom && binding.ActionName != null ? binding.ActionName : binding.Action)}");
        binding.ActionInternal?.Invoke();
    }
}

public class KeysConfig
{
    /// <summary>
    /// Currently configured key bindings
    /// (Normally you should not access this directly)
    /// </summary>
    public List<KeyBinding> Keys            { get ; set; }

    Player player;

    public KeysConfig() { }

    public KeysConfig Clone()
    {
        KeysConfig keys = (KeysConfig) MemberwiseClone();
        keys.player = null;
        keys.Keys = null;
        return keys;
    }

    internal void SetPlayer(Player player)
    {
        Keys ??= new List<KeyBinding>();

        if (!player.Config.Loaded && Keys.Count == 0)
            LoadDefault();

        this.player = player;

        foreach(var binding in Keys)
        {
            if (binding.Action != KeyBindingAction.Custom)
                binding.ActionInternal = GetKeyBindingAction(binding.Action);
        }
    }

    /// <summary>
    /// Adds a custom keybinding
    /// </summary>
    /// <param name="key">The key to bind</param>
    /// <param name="isKeyUp">If should fire on each keydown or just on keyup</param>
    /// <param name="action">The action to execute</param>
    /// <param name="actionName">A unique name to be able to identify it</param>
    /// <param name="alt">If Alt should be pressed</param>
    /// <param name="ctrl">If Ctrl should be pressed</param>
    /// <param name="shift">If Shift should be pressed</param>
    /// <exception cref="Exception">Keybinding already exists</exception>
    public void AddCustom(Key key, bool isKeyUp, Action action, string actionName, bool alt = false, bool ctrl = false, bool shift = false)
    {
        for (int i=0; i<Keys.Count; i++)
            if (Keys[i].Key == key && Keys[i].Alt == alt && Keys[i].Ctrl == ctrl && Keys[i].Shift == shift)
            {
                Keys[i].IsKeyUp = isKeyUp;
                Keys[i].Action = KeyBindingAction.Custom;
                Keys[i].ActionName = actionName;
                Keys[i].ActionInternal = action;

                return;
            }

        Keys.Add(new KeyBinding() { Alt = alt, Ctrl = ctrl, Shift = shift, Key = key, IsKeyUp = isKeyUp, Action = KeyBindingAction.Custom, ActionName = actionName, ActionInternal = action });
    }

    /// <summary>
    /// Adds a new key binding
    /// </summary>
    /// <param name="key">The key to bind</param>
    /// <param name="action">Which action from the available to assign</param>
    /// <param name="alt">If Alt should be pressed</param>
    /// <param name="ctrl">If Ctrl should be pressed</param>
    /// <param name="shift">If Shift should be pressed</param>
    /// <exception cref="Exception">Keybinding already exists</exception>
    public void Add(Key key, KeyBindingAction action, bool alt = false, bool ctrl = false, bool shift = false)
    {
        for (int i=0; i<Keys.Count; i++)
            if (Keys[i].Key == key && Keys[i].Alt == alt && Keys[i].Ctrl == ctrl && Keys[i].Shift == shift)
            {
                Keys[i].IsKeyUp = isKeyUpBinding.Contains(action);
                Keys[i].Action = action;
                Keys[i].ActionInternal = player != null ? GetKeyBindingAction(action) : null;

                return;
            }

        if (player == null)
            Keys.Add(new KeyBinding() { Alt = alt, Ctrl = ctrl, Shift = shift, Key = key, IsKeyUp = isKeyUpBinding.Contains(action), Action = action });
        else
            Keys.Add(new KeyBinding() { Alt = alt, Ctrl = ctrl, Shift = shift, Key = key, IsKeyUp = isKeyUpBinding.Contains(action), Action = action, ActionInternal = GetKeyBindingAction(action) });
    }

    public bool Exists(string actionName)
    {
        foreach (var keybinding in Keys)
            if (keybinding.ActionName == actionName)
                return true;

        return false;
    }

    public KeyBinding Get(string actionName)
    {
        foreach (var keybinding in Keys)
            if (keybinding.ActionName == actionName)
                return keybinding;

        return null;
    }

    /// <summary>
    /// Removes a binding based on Key/Ctrl combination
    /// </summary>
    /// <param name="key">The assigned key</param>
    /// <param name="alt">If Alt is assigned</param>
    /// <param name="ctrl">If Ctrl is assigned</param>
    /// <param name="shift">If Shift is assigned</param>
    public void Remove(Key key, bool alt = false, bool ctrl = false, bool shift = false)
    {
        for (int i=Keys.Count-1; i >=0; i--)
            if (Keys[i].Key == key && Keys[i].Alt == alt && Keys[i].Ctrl == ctrl && Keys[i].Shift == shift)
                Keys.RemoveAt(i);
    }

    /// <summary>
    /// Removes a binding based on assigned action
    /// </summary>
    /// <param name="action">The assigned action</param>
    public void Remove(KeyBindingAction action)
    {
        for (int i=Keys.Count-1; i >=0; i--)
            if (Keys[i].Action == action)
                Keys.RemoveAt(i);
    }

    /// <summary>
    /// Removes a binding based on assigned action's name
    /// </summary>
    /// <param name="actionName">The assigned action's name</param>
    public void Remove(string actionName)
    {
        for (int i=Keys.Count-1; i >=0; i--)
            if (Keys[i].ActionName == actionName)
                Keys.RemoveAt(i);
    }

    /// <summary>
    /// Removes all the bindings
    /// </summary>
    public void RemoveAll() => Keys.Clear();

    /// <summary>
    /// Resets to default bindings
    /// </summary>
    public void LoadDefault()
    {
        if (Keys == null)
            Keys = new List<KeyBinding>();
        else
            Keys.Clear();

        Add(Key.OemSemicolon,       KeyBindingAction.SubsDelayRemovePrimary);
        Add(Key.OemQuotes,          KeyBindingAction.SubsDelayAddPrimary);
        Add(Key.OemSemicolon,       KeyBindingAction.SubsDelayRemoveSecondary, shift: true);
        Add(Key.OemQuotes,          KeyBindingAction.SubsDelayAddSecondary, shift: true);

        Add(Key.A,                  KeyBindingAction.SubsPrevSeek);
        Add(Key.S,                  KeyBindingAction.SubsCurSeek);
        Add(Key.D,                  KeyBindingAction.SubsNextSeek);

        Add(Key.J,                  KeyBindingAction.SubsPrevSeek);
        Add(Key.K,                  KeyBindingAction.SubsCurSeek);
        Add(Key.L,                  KeyBindingAction.SubsNextSeek);

        // Mouse backford/forward button
        Add(Key.Left,               KeyBindingAction.SubsPrevSeekFallback, alt: true);
        Add(Key.Right,              KeyBindingAction.SubsNextSeekFallback, alt: true);

        Add(Key.V,                  KeyBindingAction.OpenFromClipboardSafe, ctrl: true);
        Add(Key.O,                  KeyBindingAction.OpenFromFileDialog, ctrl: true);
        Add(Key.C,                  KeyBindingAction.CopyToClipboard, ctrl: true, shift: true);

        Add(Key.Left,               KeyBindingAction.SeekBackward2);
        Add(Key.Left,               KeyBindingAction.SeekBackward, ctrl: true);
        Add(Key.Right,              KeyBindingAction.SeekForward2);
        Add(Key.Right,              KeyBindingAction.SeekForward, ctrl: true);

        Add(Key.S,                  KeyBindingAction.ToggleSeekAccurate, ctrl: true);

        Add(Key.OemPlus,            KeyBindingAction.SpeedAdd);
        Add(Key.OemPlus,            KeyBindingAction.SpeedAdd2, shift: true);
        Add(Key.OemMinus,           KeyBindingAction.SpeedRemove);
        Add(Key.OemMinus,           KeyBindingAction.SpeedRemove2, shift: true);

        Add(Key.OemPlus,            KeyBindingAction.ZoomIn, ctrl: true);
        Add(Key.OemMinus,           KeyBindingAction.ZoomOut, ctrl: true);

        Add(Key.F,                  KeyBindingAction.ToggleFullScreen);

        Add(Key.Space,              KeyBindingAction.TogglePlayPause);
        Add(Key.MediaPlayPause,     KeyBindingAction.TogglePlayPause);
        Add(Key.Play,               KeyBindingAction.TogglePlayPause);

        Add(Key.H,                  KeyBindingAction.ToggleSubtitlesVisibility);
        Add(Key.V,                  KeyBindingAction.ToggleSubtitlesVisibility);

        Add(Key.M,                  KeyBindingAction.ToggleMute);
        Add(Key.Up,                 KeyBindingAction.VolumeUp);
        Add(Key.Down,               KeyBindingAction.VolumeDown);

        Add(Key.D0,                 KeyBindingAction.ResetAll);

        Add(Key.Escape,             KeyBindingAction.NormalScreen);
        Add(Key.Q,                  KeyBindingAction.Stop, ctrl: true);
    }

    public Action GetKeyBindingAction(KeyBindingAction action)
    {
        switch (action)
        {
            case KeyBindingAction.ForceIdle:
                return player.Activity.ForceIdle;
            case KeyBindingAction.ForceActive:
                return player.Activity.ForceActive;
            case KeyBindingAction.ForceFullActive:
                return player.Activity.ForceFullActive;

            case KeyBindingAction.AudioDelayAdd:
                return player.Audio.DelayAdd;
            case KeyBindingAction.AudioDelayRemove:
                return player.Audio.DelayRemove;
            case KeyBindingAction.AudioDelayAdd2:
                return player.Audio.DelayAdd2;
            case KeyBindingAction.AudioDelayRemove2:
                return player.Audio.DelayRemove2;
            case KeyBindingAction.ToggleAudio:
                return player.Audio.Toggle;
            case KeyBindingAction.ToggleMute:
                return player.Audio.ToggleMute;
            case KeyBindingAction.VolumeUp:
                return player.Audio.VolumeUp;
            case KeyBindingAction.VolumeDown:
                return player.Audio.VolumeDown;

            case KeyBindingAction.ToggleVideo:
                return player.Video.Toggle;
            case KeyBindingAction.ToggleKeepRatio:
                return player.Video.ToggleKeepRatio;
            case KeyBindingAction.ToggleVideoAcceleration:
                return player.Video.ToggleVideoAcceleration;

            case KeyBindingAction.SubsDelayAddPrimary:
                return player.Subtitles.DelayAddPrimary;
            case KeyBindingAction.SubsDelayRemovePrimary:
                return player.Subtitles.DelayRemovePrimary;
            case KeyBindingAction.SubsDelayAdd2Primary:
                return player.Subtitles.DelayAdd2Primary;
            case KeyBindingAction.SubsDelayRemove2Primary:
                return player.Subtitles.DelayRemove2Primary;

            case KeyBindingAction.SubsDelayAddSecondary:
                return player.Subtitles.DelayAddSecondary;
            case KeyBindingAction.SubsDelayRemoveSecondary:
                return player.Subtitles.DelayRemoveSecondary;
            case KeyBindingAction.SubsDelayAdd2Secondary:
                return player.Subtitles.DelayAdd2Secondary;
            case KeyBindingAction.SubsDelayRemove2Secondary:
                return player.Subtitles.DelayRemove2Secondary;

            case KeyBindingAction.ToggleSubtitlesVisibility:
                return player.Subtitles.ToggleVisibility;
            case KeyBindingAction.ToggleSubtitlesVisibilityPrimary:
                return player.Subtitles.ToggleVisibilityPrimary;
            case KeyBindingAction.ToggleSubtitlesVisibilitySecondary:
                return player.Subtitles.ToggleVisibilitySecondary;

            case KeyBindingAction.OpenFromClipboard:
                return player.OpenFromClipboard;
            case KeyBindingAction.OpenFromClipboardSafe:
                return player.OpenFromClipboardSafe;

            case KeyBindingAction.OpenFromFileDialog:
                return player.OpenFromFileDialog;

            case KeyBindingAction.CopyToClipboard:
                return player.CopyToClipboard;

            case KeyBindingAction.CopyItemToClipboard:
                return player.CopyItemToClipboard;

            case KeyBindingAction.Flush:
                return player.Flush;

            case KeyBindingAction.Stop:
                return player.Stop;

            case KeyBindingAction.Pause:
                return player.Pause;

            case KeyBindingAction.Play:
                return player.Play;

            case KeyBindingAction.TogglePlayPause:
                return player.TogglePlayPause;

            case KeyBindingAction.TakeSnapshot:
                return player.Commands.TakeSnapshotAction;

            case KeyBindingAction.NormalScreen:
                return player.NormalScreen;

            case KeyBindingAction.FullScreen:
                return player.FullScreen;

            case KeyBindingAction.ToggleFullScreen:
                return player.ToggleFullScreen;

            case KeyBindingAction.ToggleRecording:
                return player.ToggleRecording;

            case KeyBindingAction.ToggleReversePlayback:
                return player.ToggleReversePlayback;

            case KeyBindingAction.ToggleLoopPlayback:
                return player.ToggleLoopPlayback;

            case KeyBindingAction.ToggleSeekAccurate:
                return player.ToggleSeekAccurate;

            case KeyBindingAction.SeekBackward:
                return player.SeekBackward;

            case KeyBindingAction.SeekForward:
                return player.SeekForward;

            case KeyBindingAction.SeekBackward2:
                return player.SeekBackward2;

            case KeyBindingAction.SeekForward2:
                return player.SeekForward2;

            case KeyBindingAction.SeekBackward3:
                return player.SeekBackward3;

            case KeyBindingAction.SeekForward3:
                return player.SeekForward3;

            case KeyBindingAction.SeekBackward4:
                return player.SeekBackward4;

            case KeyBindingAction.SeekForward4:
                return player.SeekForward4;

            case KeyBindingAction.SubsCurSeek:
                return player.Subtitles.CurSeek;
            case KeyBindingAction.SubsPrevSeek:
                return player.Subtitles.PrevSeek;
            case KeyBindingAction.SubsNextSeek:
                return player.Subtitles.NextSeek;
            case KeyBindingAction.SubsNextSeekFallback:
                return player.Subtitles.NextSeekFallback;
            case KeyBindingAction.SubsPrevSeekFallback:
                return player.Subtitles.PrevSeekFallback;

            case KeyBindingAction.SubsCurSeek2:
                return player.Subtitles.CurSeek2;
            case KeyBindingAction.SubsPrevSeek2:
                return player.Subtitles.PrevSeek2;
            case KeyBindingAction.SubsNextSeek2:
                return player.Subtitles.NextSeek2;
            case KeyBindingAction.SubsNextSeekFallback2:
                return player.Subtitles.NextSeekFallback2;
            case KeyBindingAction.SubsPrevSeekFallback2:
                return player.Subtitles.PrevSeekFallback2;

            case KeyBindingAction.SpeedAdd:
                return player.SpeedUp;

                case KeyBindingAction.SpeedAdd2:
                return player.SpeedUp2;

            case KeyBindingAction.SpeedRemove:
                return player.SpeedDown;

                case KeyBindingAction.SpeedRemove2:
                return player.SpeedDown2;

            case KeyBindingAction.ShowPrevFrame:
                return player.ShowFramePrev;

            case KeyBindingAction.ShowNextFrame:
                return player.ShowFrameNext;

            case KeyBindingAction.ZoomIn:
                return player.ZoomIn;

            case KeyBindingAction.ZoomOut:
                return player.ZoomOut;

            case KeyBindingAction.ResetAll:
                return player.ResetAll;

            case KeyBindingAction.ResetSpeed:
                return player.ResetSpeed;

            case KeyBindingAction.ResetRotation:
                return player.ResetRotation;

            case KeyBindingAction.ResetZoom:
                return player.ResetZoom;
        }

        return null;
    }
    private static HashSet<KeyBindingAction> isKeyUpBinding = new()
    {
        // TODO: Should Fire once one KeyDown and not again until KeyUp is fired (in case of Tasks keep track of already running actions?)

        // Having issues with alt/ctrl/shift (should save state of alt/ctrl/shift on keydown and not checked on keyup)

        { KeyBindingAction.OpenFromClipboard },
        { KeyBindingAction.OpenFromClipboardSafe },
        { KeyBindingAction.OpenFromFileDialog },
        { KeyBindingAction.CopyToClipboard },
        { KeyBindingAction.TakeSnapshot },
        { KeyBindingAction.NormalScreen },
        { KeyBindingAction.FullScreen },
        { KeyBindingAction.ToggleFullScreen },
        { KeyBindingAction.ToggleAudio },
        { KeyBindingAction.ToggleVideo },
        { KeyBindingAction.ToggleKeepRatio },
        { KeyBindingAction.ToggleVideoAcceleration },
        { KeyBindingAction.ToggleSubtitlesVisibility },
        { KeyBindingAction.ToggleSubtitlesVisibilityPrimary },
        { KeyBindingAction.ToggleSubtitlesVisibilitySecondary },
        { KeyBindingAction.ToggleMute },
        { KeyBindingAction.TogglePlayPause },
        { KeyBindingAction.ToggleRecording },
        { KeyBindingAction.ToggleReversePlayback },
        { KeyBindingAction.ToggleLoopPlayback },
        { KeyBindingAction.Play },
        { KeyBindingAction.Pause },
        { KeyBindingAction.Stop },
        { KeyBindingAction.Flush },
        { KeyBindingAction.ToggleSeekAccurate },
        { KeyBindingAction.SpeedAdd },
        { KeyBindingAction.SpeedAdd2 },
        { KeyBindingAction.SpeedRemove },
        { KeyBindingAction.SpeedRemove2 },
        { KeyBindingAction.ResetAll },
        { KeyBindingAction.ResetSpeed },
        { KeyBindingAction.ResetRotation },
        { KeyBindingAction.ResetZoom },
        { KeyBindingAction.ForceIdle },
        { KeyBindingAction.ForceActive },
        { KeyBindingAction.ForceFullActive }
    };
}
public class KeyBinding
{
    public bool             IsEnabled       { get; set; } = true;
    public bool             Alt             { get; set; }
    public bool             Ctrl            { get; set; }
    public bool             Shift           { get; set; }
    public Key              Key             { get; set; }
    public KeyBindingAction Action          { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string ActionName
    {
        get => Action == KeyBindingAction.Custom ? field : null;
        set;
    }

    public bool             IsKeyUp         { get; set; }

    /// <summary>
    /// Sets action for custom key binding
    /// </summary>
    /// <param name="action"></param>
    /// <param name="isKeyUp"></param>
    public void SetAction(Action action, bool isKeyUp)
    {
        ActionInternal  = action;
        IsKeyUp = isKeyUp;
    }

    [JsonIgnore]
    public Action ActionInternal { get; internal set; }
}

public enum KeyBindingAction
{
    [Description(nameof(Custom))]
    Custom,
    [Description("Set Activity to Idle forcibly")]
    ForceIdle,
    [Description("Set Activity to Active forcibly")]
    ForceActive,
    [Description("Set Activity to FullActive forcibly")]
    ForceFullActive,

    [Description("Increase Audio Delay (1)")]
    AudioDelayAdd,
    [Description("Increase Audio Delay (2)")]
    AudioDelayAdd2,
    [Description("Decrease Audio Delay (1)")]
    AudioDelayRemove,
    [Description("Decrease Audio Delay (2)")]
    AudioDelayRemove2,

    [Description("Toggle Audio Mute/Unmute")]
    ToggleMute,
    [Description("Volume Up (1)")]
    VolumeUp,
    [Description("Volume Down (1)")]
    VolumeDown,

    [Description("Increase Primary Subtitles Delay (1)")]
    SubsDelayAddPrimary,
    [Description("Increase Primary Subtitles Delay (2)")]
    SubsDelayAdd2Primary,
    [Description("Decrease Primary Subtitles Delay (1)")]
    SubsDelayRemovePrimary,
    [Description("Decrease Primary Subtitles Delay (2)")]
    SubsDelayRemove2Primary,
    [Description("Increase Secondary Subtitles Delay (1)")]
    SubsDelayAddSecondary,
    [Description("Increase Secondary Subtitles Delay (2)")]
    SubsDelayAdd2Secondary,
    [Description("Decrease Secondary Subtitles Delay (1)")]
    SubsDelayRemoveSecondary,
    [Description("Decrease Secondary Subtitles Delay (2)")]
    SubsDelayRemove2Secondary,

    [Description("Copy Opened Item to Clipboard")]
    CopyToClipboard,
    [Description("Copy Current Played Item to Clipboard")]
    CopyItemToClipboard,
    [Description("Open a media from clipboard")]
    OpenFromClipboard,
    [Description("Open a media from clipboard if not open")]
    OpenFromClipboardSafe,
    [Description("Open a media from file dialog")]
    OpenFromFileDialog,

    [Description("Stop playback")]
    Stop,
    [Description("Pause playback")]
    Pause,
    [Description("Play playback")]
    Play,
    [Description("Toggle play playback")]
    TogglePlayPause,

    [Description("Toggle reverse playback")]
    ToggleReversePlayback,
    [Description("Toggle loop playback")]
    ToggleLoopPlayback,
    [Description("Flushes the buffer of the player")]
    Flush,
    [Description("Take snapshot")]
    TakeSnapshot,
    [Description("Change to NormalScreen")]
    NormalScreen,
    [Description("Change to FullScreen")]
    FullScreen,
    [Description("Toggle NormalScreen / FullScreen")]
    ToggleFullScreen,

    [Description("Toggle Audio Enabled")]
    ToggleAudio,
    [Description("Toggle Video Enabled")]
    ToggleVideo,

    [Description("Toggle All Subtitles Visibility")]
    ToggleSubtitlesVisibility,
    [Description("Toggle Primary Subtitles Visibility")]
    ToggleSubtitlesVisibilityPrimary,
    [Description("Toggle Secondary Subtitles Visibility")]
    ToggleSubtitlesVisibilitySecondary,

    [Description("Toggle Keep Aspect Ratio")]
    ToggleKeepRatio,
    [Description("Toggle Video Acceleration")]
    ToggleVideoAcceleration,
    [Description("Toggle Recording")]
    ToggleRecording,
    [Description("Toggle Always Seek Accurate Mode")]
    ToggleSeekAccurate,

    [Description("Seek forwards (1)")]
    SeekForward,
    [Description("Seek backwards (1)")]
    SeekBackward,
    [Description("Seek forwards (2)")]
    SeekForward2,
    [Description("Seek backwards (2)")]
    SeekBackward2,
    [Description("Seek forwards (3)")]
    SeekForward3,
    [Description("Seek backwards (3)")]
    SeekBackward3,
    [Description("Seek forwards (4)")]
    SeekForward4,
    [Description("Seek backwards (4)")]
    SeekBackward4,

    [Description("Seek to the previous subtitle")]
    SubsPrevSeek,
    [Description("Seek to the current subtitle")]
    SubsCurSeek,
    [Description("Seek to the next subtitle")]
    SubsNextSeek,
    [Description("Seek to the previous subtitle or seek backwards")]
    SubsPrevSeekFallback,
    [Description("Seek to the next subtitle or seek forwards")]
    SubsNextSeekFallback,

    [Description("Seek to the previous secondary subtitle")]
    SubsPrevSeek2,
    [Description("Seek to the current secondary subtitle")]
    SubsCurSeek2,
    [Description("Seek to the next secondary subtitle")]
    SubsNextSeek2,
    [Description("Seek to the previous secondary subtitle or seek backwards")]
    SubsPrevSeekFallback2,
    [Description("Seek to the next secondary subtitle or seek forwards")]
    SubsNextSeekFallback2,

    [Description("Speed Up (1)")]
    SpeedAdd,
    [Description("Speed Up (2)")]
    SpeedAdd2,

    [Description("Speed Down (1)")]
    SpeedRemove,
    [Description("Speed Down (2)")]
    SpeedRemove2,

    [Description("Show Next Frame")]
    ShowNextFrame,
    [Description("Show Previous Frame")]
    ShowPrevFrame,

    [Description("Reset Zoom / Rotation / Speed")]
    ResetAll,
    [Description("Reset Speed")]
    ResetSpeed,
    [Description("Reset Rotation")]
    ResetRotation,
    [Description("Reset Zoom")]
    ResetZoom,

    [Description("Zoom In (1)")]
    ZoomIn,
    [Description("Zoom Out (1)")]
    ZoomOut,
}

using System.Threading.Tasks;
using System.Windows.Input;

using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaPlayer;

public class Commands
{
    public ICommand AudioDelaySet           { get; set; }
    public ICommand AudioDelaySet2          { get; set; }
    public ICommand AudioDelayAdd           { get; set; }
    public ICommand AudioDelayAdd2          { get; set; }
    public ICommand AudioDelayRemove        { get; set; }
    public ICommand AudioDelayRemove2       { get; set; }

    public ICommand SubtitlesDelaySetPrimary       { get; set; }
    public ICommand SubtitlesDelaySet2Primary      { get; set; }
    public ICommand SubtitlesDelayAddPrimary       { get; set; }
    public ICommand SubtitlesDelayAdd2Primary      { get; set; }
    public ICommand SubtitlesDelayRemovePrimary    { get; set; }
    public ICommand SubtitlesDelayRemove2Primary   { get; set; }

    public ICommand SubtitlesDelaySetSecondary     { get; set; }
    public ICommand SubtitlesDelaySet2Secondary    { get; set; }
    public ICommand SubtitlesDelayAddSecondary     { get; set; }
    public ICommand SubtitlesDelayAdd2Secondary    { get; set; }
    public ICommand SubtitlesDelayRemoveSecondary  { get; set; }
    public ICommand SubtitlesDelayRemove2Secondary { get; set; }

    public ICommand OpenSubtitles           { get; set; }
    public ICommand OpenSubtitlesASR        { get; set; }
    public ICommand SubtitlesOff            { get; set; }

    public ICommand Open                    { get; set; }
    public ICommand OpenFromClipboard       { get; set; }
    public ICommand OpenFromFileDialog      { get; set; }
    public ICommand Reopen                  { get; set; }
    public ICommand CopyToClipboard         { get; set; }
    public ICommand CopyItemToClipboard     { get; set; }

    public ICommand Play                    { get; set; }
    public ICommand Pause                   { get; set; }
    public ICommand Stop                    { get; set; }
    public ICommand TogglePlayPause         { get; set; }

    public ICommand SeekBackward            { get; set; }
    public ICommand SeekBackward2           { get; set; }
    public ICommand SeekBackward3           { get; set; }
    public ICommand SeekBackward4           { get; set; }
    public ICommand SeekForward             { get; set; }
    public ICommand SeekForward2            { get; set; }
    public ICommand SeekForward3            { get; set; }
    public ICommand SeekForward4            { get; set; }
    public ICommand SeekToChapter           { get; set; }

    public ICommand ShowFramePrev           { get; set; }
    public ICommand ShowFrameNext           { get; set; }

    public ICommand NormalScreen            { get; set; }
    public ICommand FullScreen              { get; set; }
    public ICommand ToggleFullScreen        { get; set; }

    public ICommand ToggleReversePlayback   { get; set; }
    public ICommand ToggleLoopPlayback      { get; set; }
    public ICommand StartRecording          { get; set; }
    public ICommand StopRecording           { get; set; }
    public ICommand ToggleRecording         { get; set; }

    public ICommand TakeSnapshot            { get; set; }
    public ICommand ZoomIn                  { get; set; }
    public ICommand ZoomOut                 { get; set; }
    public ICommand RotationSet             { get; set; }
    public ICommand RotateLeft              { get; set; }
    public ICommand RotateRight             { get; set; }
    public ICommand ResetAll                { get; set; }
    public ICommand ResetSpeed              { get; set; }
    public ICommand ResetRotation           { get; set; }
    public ICommand ResetZoom               { get; set; }

    public ICommand SpeedSet                { get; set; }
    public ICommand SpeedUp                 { get; set; }
    public ICommand SpeedUp2                { get; set; }
    public ICommand SpeedDown               { get; set; }
    public ICommand SpeedDown2              { get; set; }

    public ICommand VolumeUp                { get; set; }
    public ICommand VolumeDown              { get; set; }
    public ICommand ToggleMute              { get; set; }

    public ICommand ForceIdle               { get; set; }
    public ICommand ForceActive             { get; set; }
    public ICommand ForceFullActive         { get; set; }
    public ICommand RefreshActive           { get; set; }
    public ICommand RefreshFullActive       { get; set; }

    public ICommand ResetFilter             { get; set; }

    Player player;

    public Commands(Player player)
    {
        this.player = player;

        Open                    = new RelayCommand(OpenAction);
        OpenFromClipboard       = new RelayCommandSimple(player.OpenFromClipboard);
        OpenFromFileDialog      = new RelayCommandSimple(player.OpenFromFileDialog);
        Reopen                  = new RelayCommand(ReopenAction);
        CopyToClipboard         = new RelayCommandSimple(player.CopyToClipboard);
        CopyItemToClipboard     = new RelayCommandSimple(player.CopyItemToClipboard);

        Play                    = new RelayCommandSimple(player.Play);
        Pause                   = new RelayCommandSimple(player.Pause);
        TogglePlayPause         = new RelayCommandSimple(player.TogglePlayPause);
        Stop                    = new RelayCommandSimple(player.Stop);

        SeekBackward            = new RelayCommandSimple(player.SeekBackward);
        SeekBackward2           = new RelayCommandSimple(player.SeekBackward2);
        SeekBackward3           = new RelayCommandSimple(player.SeekBackward3);
        SeekBackward4           = new RelayCommandSimple(player.SeekBackward4);
        SeekForward             = new RelayCommandSimple(player.SeekForward);
        SeekForward2            = new RelayCommandSimple(player.SeekForward2);
        SeekForward3            = new RelayCommandSimple(player.SeekForward3);
        SeekForward4            = new RelayCommandSimple(player.SeekForward4);
        SeekToChapter           = new RelayCommand(SeekToChapterAction);

        ShowFrameNext           = new RelayCommandSimple(player.ShowFrameNext);
        ShowFramePrev           = new RelayCommandSimple(player.ShowFramePrev);

        NormalScreen            = new RelayCommandSimple(player.NormalScreen);
        FullScreen              = new RelayCommandSimple(player.FullScreen);
        ToggleFullScreen        = new RelayCommandSimple(player.ToggleFullScreen);

        ToggleReversePlayback   = new RelayCommandSimple(player.ToggleReversePlayback);
        ToggleLoopPlayback      = new RelayCommandSimple(player.ToggleLoopPlayback);
        StartRecording          = new RelayCommandSimple(player.StartRecording);
        StopRecording           = new RelayCommandSimple(player.StopRecording);
        ToggleRecording         = new RelayCommandSimple(player.ToggleRecording);

        TakeSnapshot            = new RelayCommandSimple(TakeSnapshotAction);
        ZoomIn                  = new RelayCommandSimple(player.ZoomIn);
        ZoomOut                 = new RelayCommandSimple(player.ZoomOut);
        RotationSet             = new RelayCommand(RotationSetAction);
        RotateLeft              = new RelayCommandSimple(player.RotateLeft);
        RotateRight             = new RelayCommandSimple(player.RotateRight);
        ResetAll                = new RelayCommandSimple(player.ResetAll);
        ResetSpeed              = new RelayCommandSimple(player.ResetSpeed);
        ResetRotation           = new RelayCommandSimple(player.ResetRotation);
        ResetZoom               = new RelayCommandSimple(player.ResetZoom);

        SpeedSet                = new RelayCommand(SpeedSetAction);
        SpeedUp                 = new RelayCommandSimple(player.SpeedUp);
        SpeedDown               = new RelayCommandSimple(player.SpeedDown);
        SpeedUp2                = new RelayCommandSimple(player.SpeedUp2);
        SpeedDown2              = new RelayCommandSimple(player.SpeedDown2);

        VolumeUp                = new RelayCommandSimple(player.Audio.VolumeUp);
        VolumeDown              = new RelayCommandSimple(player.Audio.VolumeDown);
        ToggleMute              = new RelayCommandSimple(player.Audio.ToggleMute);

        AudioDelaySet           = new RelayCommand(AudioDelaySetAction);
        AudioDelaySet2          = new RelayCommand(AudioDelaySetAction2);
        AudioDelayAdd           = new RelayCommandSimple(player.Audio.DelayAdd);
        AudioDelayAdd2          = new RelayCommandSimple(player.Audio.DelayAdd2);
        AudioDelayRemove        = new RelayCommandSimple(player.Audio.DelayRemove);
        AudioDelayRemove2       = new RelayCommandSimple(player.Audio.DelayRemove2);

        SubtitlesDelaySetPrimary       = new RelayCommand(SubtitlesDelaySetActionPrimary);
        SubtitlesDelaySet2Primary      = new RelayCommand(SubtitlesDelaySetAction2Primary);
        SubtitlesDelayAddPrimary       = new RelayCommandSimple(player.Subtitles.DelayAddPrimary);
        SubtitlesDelayAdd2Primary      = new RelayCommandSimple(player.Subtitles.DelayAdd2Primary);
        SubtitlesDelayRemovePrimary    = new RelayCommandSimple(player.Subtitles.DelayRemovePrimary);
        SubtitlesDelayRemove2Primary   = new RelayCommandSimple(player.Subtitles.DelayRemove2Primary);

        SubtitlesDelaySetSecondary     = new RelayCommand(SubtitlesDelaySetActionSecondary);
        SubtitlesDelaySet2Secondary    = new RelayCommand(SubtitlesDelaySetAction2Secondary);
        SubtitlesDelayAddSecondary     = new RelayCommandSimple(player.Subtitles.DelayAddSecondary);
        SubtitlesDelayAdd2Secondary    = new RelayCommandSimple(player.Subtitles.DelayAdd2Secondary);
        SubtitlesDelayRemoveSecondary  = new RelayCommandSimple(player.Subtitles.DelayRemoveSecondary);
        SubtitlesDelayRemove2Secondary = new RelayCommandSimple(player.Subtitles.DelayRemove2Secondary);

        OpenSubtitles           = new RelayCommand(OpenSubtitlesAction);
        OpenSubtitlesASR        = new RelayCommand(OpenSubtitlesASRAction);
        SubtitlesOff            = new RelayCommand(SubtitlesOffAction);

        ForceIdle               = new RelayCommandSimple(player.Activity.ForceIdle);
        ForceActive             = new RelayCommandSimple(player.Activity.ForceActive);
        ForceFullActive         = new RelayCommandSimple(player.Activity.ForceFullActive);
        RefreshActive           = new RelayCommandSimple(player.Activity.RefreshActive);
        RefreshFullActive       = new RelayCommandSimple(player.Activity.RefreshFullActive);

        ResetFilter             = new RelayCommand(ResetFilterAction);
    }

    private void RotationSetAction(object obj)
        => player.Rotation = uint.Parse(obj.ToString());

    private void ResetFilterAction(object filter)
        => player.Config.Video.Filters[(VideoFilters)filter].Value = player.Config.Video.Filters[(VideoFilters)filter].DefaultValue;

    public void SpeedSetAction(object speed)
    {
        string speedstr = speed.ToString().Replace(',', '.');
        if (double.TryParse(speedstr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double value))
            player.Speed = value;
    }

    public void AudioDelaySetAction(object delay)
        => player.Config.Audio.Delay = int.Parse(delay.ToString()) * (long)10000;
    public void AudioDelaySetAction2(object delay)
        => player.Config.Audio.Delay += int.Parse(delay.ToString()) * (long)10000;

    public void SubtitlesDelaySetActionPrimary(object delay)
        => player.Config.Subtitles[0].Delay = int.Parse(delay.ToString()) * (long)10000;
    public void SubtitlesDelaySetAction2Primary(object delay)
        => player.Config.Subtitles[0].Delay += int.Parse(delay.ToString()) * (long)10000;

    // TODO: L: refactor
    public void SubtitlesDelaySetActionSecondary(object delay)
        => player.Config.Subtitles[1].Delay = int.Parse(delay.ToString()) * (long)10000;
    public void SubtitlesDelaySetAction2Secondary(object delay)
        => player.Config.Subtitles[1].Delay += int.Parse(delay.ToString()) * (long)10000;

    public void TakeSnapshotAction() => Task.Run(() => { try { player.TakeSnapshotToFile(); } catch { } });

    public void SeekToChapterAction(object chapter)
    {
        if (player.Chapters == null || player.Chapters.Count == 0)
            return;

        if (chapter is MediaFramework.MediaDemuxer.Demuxer.Chapter)
            player.SeekToChapter((MediaFramework.MediaDemuxer.Demuxer.Chapter)chapter);
        else if (int.TryParse(chapter.ToString(), out int chapterId) && chapterId < player.Chapters.Count)
            player.SeekToChapter(player.Chapters[chapterId]);
    }

    public void OpenSubtitlesAction(object input)
    {
        if (input is not ValueTuple<object, object, object> tuple)
        {
            return;
        }

        if (tuple is { Item1: string, Item2: SubtitlesStream, Item3: SelectSubMethod })
        {
            var subIndex = int.Parse((string)tuple.Item1);
            var stream = (SubtitlesStream)tuple.Item2;
            var selectSubMethod = (SelectSubMethod)tuple.Item3;

            if (selectSubMethod == SelectSubMethod.OCR)
            {
                if (!TryInitializeOCR(subIndex, stream.Language))
                {
                    return;
                }
            }

            SubtitlesSelectedHelper.Set(subIndex, (stream.StreamIndex, null));
            SubtitlesSelectedHelper.SetMethod(subIndex, selectSubMethod);
            SubtitlesSelectedHelper.CurIndex = subIndex;
        }
        else if (tuple is { Item1: string, Item2: ExternalSubtitlesStream, Item3: SelectSubMethod })
        {
            var subIndex = int.Parse((string)tuple.Item1);
            var stream = (ExternalSubtitlesStream)tuple.Item2;
            var selectSubMethod = (SelectSubMethod)tuple.Item3;

            if (selectSubMethod == SelectSubMethod.OCR)
            {
                if (!TryInitializeOCR(subIndex, stream.Language))
                {
                    return;
                }
            }

            SubtitlesSelectedHelper.Set(subIndex, (null, stream));
            SubtitlesSelectedHelper.SetMethod(subIndex, selectSubMethod);
            SubtitlesSelectedHelper.CurIndex = subIndex;
        }

        OpenAction(tuple.Item2);
        return;

        bool TryInitializeOCR(int subIndex, Language lang)
        {
            if (!player.SubtitlesOCR.TryInitialize(subIndex, lang, out string err))
            {
                player.RaiseKnownErrorOccurred(err, KnownErrorType.Configuration);
                return false;
            }

            return true;
        }
    }

    public void OpenSubtitlesASRAction(object input)
    {
        if (!int.TryParse(input.ToString(), out var subIndex))
        {
            return;
        }

        if (!player.Audio.IsOpened)
        {
            // not opened
            return;
        }

        if (!player.SubtitlesASR.CanExecute(out string err))
        {
            player.RaiseKnownErrorOccurred(err, KnownErrorType.Configuration);
            return;
        }

        if (player.IsLive)
        {
            player.RaiseKnownErrorOccurred("Currently ASR is not available for live streams.", KnownErrorType.ASR);
            return;
        }

        SubtitlesSelectedHelper.CurIndex = subIndex;

        // First, turn off existing subtitles (if not ASR)
        if (!player.Subtitles[subIndex].EnabledASR)
        {
            player.Subtitles[subIndex].Disable();
        }

        player.Subtitles[subIndex].EnableASR();
    }

    public void SubtitlesOffAction(object input)
    {
        if (int.TryParse(input.ToString(), out var subIndex))
        {
            SubtitlesSelectedHelper.CurIndex = subIndex;
            player.Subtitles[subIndex].Disable();
        }
    }

    public void OpenAction(object input)
    {
        if (input == null)
            return;

        if (input is StreamBase)
            player.OpenAsync((StreamBase)input);
        else if (input is PlaylistItem)
            player.OpenAsync((PlaylistItem)input);
        else if (input is ExternalStream)
            player.OpenAsync((ExternalStream)input);
        else if (input is System.IO.Stream)
            player.OpenAsync((System.IO.Stream)input);
        else
            player.OpenAsync(input.ToString());
    }

    public void ReopenAction(object playlistItem)
    {
        if (playlistItem == null)
            return;

        PlaylistItem item = (PlaylistItem)playlistItem;
        if (item.OpenedCounter > 0)
        {
            var session = player.GetSession(item);
            session.isReopen = true;
            session.CurTime = 0;

            // TBR: in case of disabled audio/video/subs it will save the session with them to be disabled

            // TBR: This can cause issues and it might not useful either
            //if (session.CurTime < 60 * (long)1000 * 10000)
            //    session.CurTime = 0;

            player.OpenAsync(session);
        }
        else
            player.OpenAsync(item);
    }
}

using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;

namespace FlyleafLib.MediaPlayer.Translation.Services;

public interface ITranslateService : IDisposable
{
    TranslateServiceType ServiceType { get; }

    /// <summary>
    /// Initialize
    /// </summary>
    /// <param name="src"></param>
    /// <param name="target"></param>
    /// <exception cref="TranslationConfigException">when language is not supported or configured properly</exception>
    void Initialize(Language src, TargetLanguage target);

    /// <summary>
    /// TranslateAsync
    /// </summary>
    /// <param name="text"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="TranslationException">when translation is failed</exception>
    /// <exception cref="OperationCanceledException"></exception>
    Task<string> TranslateAsync(string text, CancellationToken token);
}

[Flags]
public enum TranslateServiceType
{
    /// <summary>
    /// Google V1.
    /// </summary>
    [Description("Google V1")]
    GoogleV1 = 1 << 0,

    /// <summary>
    /// DeepL
    /// </summary>
    [Description(nameof(DeepL))]
    DeepL = 1 << 1,

    /// <summary>
    /// DeepLX
    /// https://github.com/OwO-Network/DeepLX
    /// </summary>
    [Description(nameof(DeepLX))]
    DeepLX = 1 << 2,

    /// <summary>
    /// Ollama
    /// </summary>
    [Description(nameof(Ollama))]
    Ollama = 1 << 3,

    /// <summary>
    /// LM Studio
    /// </summary>
    [Description("LM Studio")]
    LMStudio = 1 << 4,

    /// <summary>
    /// KoboldCpp
    /// </summary>
    [Description(nameof(KoboldCpp))]
    KoboldCpp = 1 << 5,

    /// <summary>
    /// OpenAI (ChatGPT)
    /// </summary>
    [Description(nameof(OpenAI))]
    OpenAI = 1 << 6,

    /// <summary>
    /// OpenAI compatible
    /// </summary>
    [Description("OpenAI Like")]
    OpenAILike = 1 << 7,

    /// <summary>
    /// Anthropic Claude
    /// </summary>
    [Description(nameof(Claude))]
    Claude = 1 << 8,

    /// <summary>
    /// LiteLLM
    /// </summary>
    [Description(nameof(LiteLLM))]
    LiteLLM = 1 << 9,
}

public static class TranslateServiceTypeExtensions
{
    public static TranslateServiceType LLMServices =>
        TranslateServiceType.Ollama |
        TranslateServiceType.LMStudio |
        TranslateServiceType.KoboldCpp |
        TranslateServiceType.OpenAI |
        TranslateServiceType.OpenAILike |
        TranslateServiceType.Claude |
        TranslateServiceType.LiteLLM;

    public static bool IsLLM(this TranslateServiceType serviceType)
    {
        return LLMServices.HasFlag(serviceType);
    }

    public static ITranslateSettings DefaultSettings(this TranslateServiceType serviceType)
    {
        switch (serviceType)
        {
            case TranslateServiceType.GoogleV1:
                return new GoogleV1TranslateSettings();
            case TranslateServiceType.DeepL:
                return new DeepLTranslateSettings();
            case TranslateServiceType.DeepLX:
                return new DeepLXTranslateSettings();
            case TranslateServiceType.Ollama:
                return new OllamaTranslateSettings();
            case TranslateServiceType.LMStudio:
                return new LMStudioTranslateSettings();
            case TranslateServiceType.KoboldCpp:
                return new KoboldCppTranslateSettings();
            case TranslateServiceType.OpenAI:
                return new OpenAITranslateSettings();
            case TranslateServiceType.OpenAILike:
                return new OpenAILikeTranslateSettings();
            case TranslateServiceType.Claude:
                return new ClaudeTranslateSettings();
            case TranslateServiceType.LiteLLM:
                return new LiteLLMTranslateSettings();
        }

        throw new InvalidOperationException();
    }
}

public static class TranslateServiceHelper
{
    /// <summary>
    /// TryGetLanguage
    /// </summary>
    /// <param name="service"></param>
    /// <param name="src"></param>
    /// <param name="target"></param>
    /// <returns></returns>
    /// <exception cref="TranslationConfigException"></exception>
    public static (TranslateLanguage srcLang, TranslateLanguage targetLang) TryGetLanguage(this ITranslateService service, Language src, TargetLanguage target)
    {
        string iso6391 = src.ISO6391;

        // TODO: L: Allow the user to choose auto-detection by translation provider?
        if (src == Language.Unknown)
        {
            throw new TranslationConfigException("source language are unknown");
        }

        if (src.ISO6391 == target.ToISO6391())
        {
            // Only chinese allow translation between regions (Simplified <-> Traditional)
            // Portuguese, French, and English are not permitted.
            // TODO: L: review this validation?
            if (target is not (TargetLanguage.ChineseSimplified or TargetLanguage.ChineseTraditional))
            {
                throw new TranslationConfigException("source and target language are same");
            }
        }

        if (!TranslateLanguage.Langs.TryGetValue(iso6391, out TranslateLanguage srcLang))
        {
            throw new TranslationConfigException($"source language is not supported: {src.TopEnglishName}");
        }

        if (!srcLang.SupportedServices.HasFlag(service.ServiceType))
        {
            throw new TranslationConfigException($"source language is not supported by {service.ServiceType}: {src.TopEnglishName}");
        }

        if (!TranslateLanguage.Langs.TryGetValue(target.ToISO6391(), out TranslateLanguage targetLang))
        {
            throw new TranslationConfigException($"target language is not supported: {target.ToString()}");
        }

        if (!targetLang.SupportedServices.HasFlag(service.ServiceType))
        {
            throw new TranslationConfigException($"target language is not supported by {service.ServiceType}: {src.TopEnglishName}");
        }

        return (srcLang, targetLang);
    }
}

public class TranslationException : Exception
{
    public TranslationException()
    {
    }

    public TranslationException(string message)
        : base(message)
    {
    }

    public TranslationException(string message, Exception inner)
        : base(message, inner)
    {
    }
}

public class TranslationConfigException : Exception
{
    public TranslationConfigException()
    {
    }

    public TranslationConfigException(string message)
        : base(message)
    {
    }

    public TranslationConfigException(string message, Exception inner)
        : base(message, inner)
    {
    }
}

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaRemuxer;
using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaPlayer;
using FlyleafLib.Plugins;

using static FlyleafLib.Logger;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaFramework.MediaContext;

public unsafe partial class DecoderContext : PluginHandler
{
    /* TODO
     *
     * 1) Lock delay on demuxers' Format Context (for network streams)
     *      Ensure we interrupt if we are planning to seek
     *      Merge Seek witih GetVideoFrame (To seek accurate or to ensure keyframe)
     *      Long delay on Enable/Disable demuxer's streams (lock might not required)
     *
     * 2) Resync implementation / CurTime
     *      Transfer player's resync implementation here
     *      Ensure we can trust CurTime on lower level (eg. on decoders - demuxers using dts)
     *
     * 3) Timestamps / Memory leak
     *      If we have embedded audio/video and the audio decoder will stop/fail for some reason the demuxer will keep filling audio packets
     *      Should also check at lower level (demuxer) to prevent wrong packet timestamps (too early or too late)
     *      This is normal if it happens on live HLS (probably an ffmpeg bug)
     */

    #region Properties
    public object               Tag                 { get; set; } // Upper Layer Object (eg. Player, Downloader) - mainly for plugins to access it
    public bool                 EnableDecoding      { get; set; }
    public new bool             Interrupt
    {
        get => base.Interrupt;
        set
        {
            base.Interrupt = value;

            if (value)
            {
                VideoDemuxer.Interrupter.ForceInterrupt = 1;
                AudioDemuxer.Interrupter.ForceInterrupt = 1;

                for (int i = 0; i < subNum; i++)
                {
                    SubtitlesDemuxers[i].Interrupter.ForceInterrupt = 1;
                }
                DataDemuxer.Interrupter.ForceInterrupt = 1;
            }
            else
            {
                VideoDemuxer.Interrupter.ForceInterrupt = 0;
                AudioDemuxer.Interrupter.ForceInterrupt = 0;

                for (int i = 0; i < subNum; i++)
                {
                    SubtitlesDemuxers[i].Interrupter.ForceInterrupt = 0;
                }
                DataDemuxer.Interrupter.ForceInterrupt = 0;
            }
        }
    }

    /// <summary>
    /// It will not resync by itself. Requires manual call to ReSync()
    /// </summary>
    public bool                 RequiresResync      { get; set; }

    public string               Extension           => VideoDemuxer.Disposed ? AudioDemuxer.Extension : VideoDemuxer.Extension;

    // Demuxers
    public Demuxer              MainDemuxer         { get; private set; }
    public Demuxer              AudioDemuxer        { get; private set; }
    public Demuxer              VideoDemuxer        { get; private set; }
    // Demuxer for external subtitles, currently not used for subtitles, just used for stream info
    // Subtitles are displayed using SubtitlesManager
    public Demuxer[]            SubtitlesDemuxers   { get; private set; }
    public SubtitlesManager     SubtitlesManager    { get; private set; }

    public SubtitlesOCR         SubtitlesOCR        { get; private set; }
    public SubtitlesASR         SubtitlesASR        { get; private set; }

    public Demuxer              DataDemuxer         { get; private set; }

    // Decoders
    public AudioDecoder         AudioDecoder        { get; private set; }
    public VideoDecoder         VideoDecoder        { get; internal set;}
    public SubtitlesDecoder[]   SubtitlesDecoders   { get; private set; }
    public DataDecoder          DataDecoder         { get; private set; }
    public DecoderBase GetDecoderPtr(StreamBase stream)
    {
        switch (stream.Type)
        {
            case MediaType.Audio:
                return AudioDecoder;
            case MediaType.Video:
                return VideoDecoder;
            case MediaType.Data:
                return DataDecoder;
            case MediaType.Subs:
                return SubtitlesDecoders[SubtitlesSelectedHelper.CurIndex];
            default:
                throw new InvalidOperationException();
        }
    }
    // Streams
    public AudioStream          AudioStream         => (VideoDemuxer?.AudioStream) ?? AudioDemuxer.AudioStream;
    public VideoStream          VideoStream         => VideoDemuxer?.VideoStream;
    public SubtitlesStream[]    SubtitlesStreams
    {
        get
        {
            SubtitlesStream[] streams = new SubtitlesStream[subNum];

            for (int i = 0; i < subNum; i++)
            {
                if (VideoDemuxer?.SubtitlesStreams?[i] != null)
                {
                    streams[i] = VideoDemuxer.SubtitlesStreams[i];
                }
                else if (SubtitlesDemuxers[i]?.SubtitlesStreams?[i] != null)
                {
                    streams[i] = SubtitlesDemuxers[i].SubtitlesStreams[i];
                }
            }

            return streams;
        }
    }
    public DataStream           DataStream          => (VideoDemuxer?.DataStream) ?? DataDemuxer.DataStream;

    public Tuple<ExternalAudioStream, int>      ClosedAudioStream       { get; private set; }
    public Tuple<ExternalVideoStream, int>      ClosedVideoStream       { get; private set; }
    #endregion

    #region Initialize
    LogHandler Log;
    bool shouldDispose;
    int subNum => Config.Subtitles.Max;

    public DecoderContext(Config config = null, int uniqueId = -1, bool enableDecoding = true) : base(config, uniqueId)
    {
        Log = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + " [DecoderContext] ");
        Playlist.decoder    = this;

        EnableDecoding      = enableDecoding;

        AudioDemuxer        = new Demuxer(Config.Demuxer, MediaType.Audio, UniqueId, EnableDecoding);
        VideoDemuxer        = new Demuxer(Config.Demuxer, MediaType.Video, UniqueId, EnableDecoding);
        SubtitlesDemuxers   = new Demuxer[subNum];
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i] = new Demuxer(Config.Demuxer, MediaType.Subs, UniqueId, EnableDecoding);
        }
        DataDemuxer         = new Demuxer(Config.Demuxer, MediaType.Data, UniqueId, EnableDecoding);

        SubtitlesManager    = new SubtitlesManager(Config, subNum);
        SubtitlesOCR        = new SubtitlesOCR(Config.Subtitles, subNum);
        SubtitlesASR        = new SubtitlesASR(SubtitlesManager, Config);

        Recorder            = new Remuxer(UniqueId);

        VideoDecoder        = new VideoDecoder(Config, UniqueId);
        AudioDecoder        = new AudioDecoder(Config, UniqueId, VideoDecoder);
        SubtitlesDecoders   = new SubtitlesDecoder[subNum];
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i] = new SubtitlesDecoder(Config, UniqueId, i);
        }
        DataDecoder         = new DataDecoder(Config, UniqueId);

        if (EnableDecoding && config.Player.Usage != MediaPlayer.Usage.Audio)
            VideoDecoder.CreateRenderer();

        VideoDecoder.recCompleted = RecordCompleted;
        AudioDecoder.recCompleted = RecordCompleted;
    }

    public void Initialize()
    {
        VideoDecoder.Renderer?.ClearScreen();
        RequiresResync = false;

        OnInitializing();
        Stop();
        OnInitialized();
    }
    public void InitializeSwitch()
    {
        VideoDecoder.Renderer?.ClearScreen();
        RequiresResync = false;
        ClosedAudioStream = null;
        ClosedVideoStream = null;

        OnInitializingSwitch();
        Stop();
        OnInitializedSwitch();
    }
    #endregion

    #region Seek
    public int Seek(long ms = -1, bool forward = false, bool seekInQueue = true)
    {
        int ret = 0;

        if (ms == -1) ms = GetCurTimeMs();

        // Review decoder locks (lockAction should be added to avoid dead locks with flush mainly before lockCodecCtx)
        AudioDecoder.resyncWithVideoRequired = false; // Temporary to avoid dead lock on AudioDecoder.lockCodecCtx
        lock (VideoDecoder.lockCodecCtx)
        lock (AudioDecoder.lockCodecCtx)
        lock (SubtitlesDecoders[0].lockCodecCtx)
        lock (SubtitlesDecoders[1].lockCodecCtx)
        lock (DataDecoder.lockCodecCtx)
        {
            long seekTimestamp = CalcSeekTimestamp(VideoDemuxer, ms, ref forward);

            // Should exclude seek in queue for all "local/fast" files
            lock (VideoDemuxer.lockActions)
            if (Playlist.InputType == InputType.Torrent || ms == 0 || !seekInQueue || VideoDemuxer.SeekInQueue(seekTimestamp, forward) != 0)
            {
                VideoDemuxer.Interrupter.ForceInterrupt = 1;
                OpenedPlugin.OnBuffering();
                lock (VideoDemuxer.lockFmtCtx)
                {
                    if (VideoDemuxer.Disposed) { VideoDemuxer.Interrupter.ForceInterrupt = 0; return -1; }
                    ret = VideoDemuxer.Seek(seekTimestamp, forward);
                }
            }

            VideoDecoder.Flush();
            if (ms == 0)
                VideoDecoder.keyPacketRequired = false; // TBR

            if (AudioStream != null && AudioDecoder.OnVideoDemuxer)
            {
                AudioDecoder.Flush();
                if (ms == 0)
                    AudioDecoder.nextPts = AudioDecoder.Stream.StartTimePts;
            }

            for (int i = 0; i < subNum; i++)
            {
                if (SubtitlesStreams[i] != null && SubtitlesDecoders[i].OnVideoDemuxer)
                {
                    SubtitlesDecoders[i].Flush();
                }
            }

            if (DataStream != null && DataDecoder.OnVideoDemuxer)
                DataDecoder.Flush();
        }

        if (AudioStream != null && !AudioDecoder.OnVideoDemuxer)
        {
            AudioDecoder.Pause();
            AudioDecoder.Flush();
            AudioDemuxer.PauseOnQueueFull = true;
            RequiresResync = true;
        }

        //for (int i = 0; i < subNum; i++)
        //{
        //    if (SubtitlesStreams[i] != null && !SubtitlesDecoders[i].OnVideoDemuxer)
        //    {
        //        SubtitlesDecoders[i].Pause();
        //        SubtitlesDecoders[i].Flush();
        //        SubtitlesDemuxers[i].PauseOnQueueFull = true;
        //        RequiresResync = true;
        //    }
        //}

        if (DataStream != null && !DataDecoder.OnVideoDemuxer)
        {
            DataDecoder.Pause();
            DataDecoder.Flush();
            DataDemuxer.PauseOnQueueFull = true;
            RequiresResync = true;
        }

        return ret;
    }
    public int SeekAudio(long ms = -1, bool forward = false)
    {
        int ret = 0;

        if (AudioDemuxer.Disposed || AudioDecoder.OnVideoDemuxer || !Config.Audio.Enabled) return -1;

        if (ms == -1) ms = GetCurTimeMs();

        long seekTimestamp = CalcSeekTimestamp(AudioDemuxer, ms, ref forward);

        AudioDecoder.resyncWithVideoRequired = false; // Temporary to avoid dead lock on AudioDecoder.lockCodecCtx
        lock (AudioDecoder.lockActions)
        lock (AudioDecoder.lockCodecCtx)
        {
            lock (AudioDemuxer.lockActions)
                if (AudioDemuxer.SeekInQueue(seekTimestamp, forward) != 0)
                    ret = AudioDemuxer.Seek(seekTimestamp, forward);

            AudioDecoder.Flush();
            if (VideoDecoder.IsRunning)
            {
                AudioDemuxer.Start();
                AudioDecoder.Start();
            }
        }

        return ret;
    }

    //public int SeekSubtitles(int subIndex = -1, long ms = -1, bool forward = false)
    //{
    //    int ret = -1;

    //    if (!Config.Subtitles.Enabled)
    //        return ret;

    //    // all sub streams
    //    int startIndex = 0;
    //    int endIndex = subNum - 1;
    //    if (subIndex != -1)
    //    {
    //        // specific sub stream
    //        startIndex = subIndex;
    //        endIndex = subIndex;
    //    }

    //    for (int i = startIndex; i <= endIndex; i++)
    //    {
    //        // Perform seek only for external subtitles
    //        if (SubtitlesDemuxers[i].Disposed || SubtitlesDecoders[i].OnVideoDemuxer)
    //        {
    //            continue;
    //        }

    //        long localMs = ms;
    //        if (localMs == -1)
    //        {
    //            localMs = GetCurTimeMs();
    //        }

    //        long seekTimestamp = CalcSeekTimestamp(SubtitlesDemuxers[i], localMs, ref forward);

    //        lock (SubtitlesDecoders[i].lockActions)
    //            lock (SubtitlesDecoders[i].lockCodecCtx)
    //            {
    //                // Currently disabled as it will fail to seek within the queue the most of the times
    //                //lock (SubtitlesDemuxer.lockActions)
    //                //if (SubtitlesDemuxer.SeekInQueue(seekTimestamp, forward) != 0)
    //                ret = SubtitlesDemuxers[i].Seek(seekTimestamp, forward);

    //                SubtitlesDecoders[i].Flush();
    //                if (VideoDecoder.IsRunning)
    //                {
    //                    SubtitlesDemuxers[i].Start();
    //                    SubtitlesDecoders[i].Start();
    //                }
    //            }
    //    }
    //    return ret;
    //}

    public int SeekData(long ms = -1, bool forward = false)
    {
        int ret = 0;

        if (DataDemuxer.Disposed || DataDecoder.OnVideoDemuxer)
            return -1;

        if (ms == -1)
            ms = GetCurTimeMs();

        long seekTimestamp = CalcSeekTimestamp(DataDemuxer, ms, ref forward);

        lock (DataDecoder.lockActions)
            lock (DataDecoder.lockCodecCtx)
            {
                ret = DataDemuxer.Seek(seekTimestamp, forward);

                DataDecoder.Flush();
                if (VideoDecoder.IsRunning)
                {
                    DataDemuxer.Start();
                    DataDecoder.Start();
                }
            }

        return ret;
    }

    public long GetCurTime()    => !VideoDemuxer.Disposed ? VideoDemuxer.CurTime : !AudioDemuxer.Disposed ? AudioDemuxer.CurTime : 0;
    public int GetCurTimeMs()   => !VideoDemuxer.Disposed ? (int)(VideoDemuxer.CurTime / 10000) : (!AudioDemuxer.Disposed ? (int)(AudioDemuxer.CurTime / 10000) : 0);

    private long CalcSeekTimestamp(Demuxer demuxer, long ms, ref bool forward)
    {
        long startTime = demuxer.hlsCtx == null ? demuxer.StartTime : demuxer.hlsCtx->first_timestamp * 10;
        long ticks = (ms * 10000) + startTime;

        if (demuxer.Type == MediaType.Audio) ticks -= Config.Audio.Delay;
        //if (demuxer.Type == MediaType.Subs)
        //{
        //    int i = SubtitlesDemuxers[0] == demuxer ? 0 : 1;
        //    ticks -= Config.Subtitles[i].Delay + (2 * 1000 * 10000); // We even want the previous subtitles
        //}

        if (ticks < startTime)
        {
            ticks = startTime;
            forward = true;
        }
        else if (ticks > startTime + (!VideoDemuxer.Disposed ? VideoDemuxer.Duration : AudioDemuxer.Duration) - (50 * 10000))
        {
            ticks = Math.Max(startTime, startTime + demuxer.Duration - (50 * 10000));
            forward = false;
        }

        return ticks;
    }
    #endregion

    #region Start/Pause/Stop
    public void Pause()
    {
        VideoDecoder.Pause();
        AudioDecoder.Pause();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i].Pause();
        }
        DataDecoder.Pause();

        VideoDemuxer.Pause();
        AudioDemuxer.Pause();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i].Pause();
        }
        DataDemuxer.Pause();
    }
    public void PauseDecoders()
    {
        VideoDecoder.Pause();
        AudioDecoder.Pause();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i].Pause();
        }
        DataDecoder.Pause();
    }
    public void PauseOnQueueFull()
    {
        VideoDemuxer.PauseOnQueueFull = true;
        AudioDemuxer.PauseOnQueueFull = true;
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i].PauseOnQueueFull = true;
        }
        DataDecoder.PauseOnQueueFull = true;
    }
    public void Start()
    {
        //if (RequiresResync) Resync();

        if (Config.Audio.Enabled)
        {
            AudioDemuxer.Start();
            AudioDecoder.Start();
        }

        if (Config.Video.Enabled)
        {
            VideoDemuxer.Start();
            VideoDecoder.Start();
        }

        if (Config.Subtitles.Enabled)
        {
            for (int i = 0; i < subNum; i++)
            {
                //if (SubtitlesStreams[i] != null && !SubtitlesDecoders[i].OnVideoDemuxer)
                //    SubtitlesDemuxers[i].Start();

                //if (SubtitlesStreams[i] != null)
                if (SubtitlesStreams[i] != null && SubtitlesDecoders[i].OnVideoDemuxer)
                    SubtitlesDecoders[i].Start();
            }
        }

        if (Config.Data.Enabled)
        {
            DataDemuxer.Start();
            DataDecoder.Start();
        }
    }
    public void Stop()
    {
        Interrupt = true;

        VideoDecoder.Dispose();
        AudioDecoder.Dispose();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i].Dispose();
        }

        DataDecoder.Dispose();
        AudioDemuxer.Dispose();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i].Dispose();
        }

        DataDemuxer.Dispose();
        VideoDemuxer.Dispose();

        Interrupt = false;
    }
    public void StopThreads()
    {
        Interrupt = true;

        VideoDecoder.Stop();
        AudioDecoder.Stop();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i].Stop();
        }

        DataDecoder.Stop();
        AudioDemuxer.Stop();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i].Stop();
        }
        DataDemuxer.Stop();
        VideoDemuxer.Stop();

        Interrupt = false;
    }
    #endregion

    public void Resync(long timestamp = -1)
    {
        bool isRunning = VideoDemuxer.IsRunning;

        if (AudioStream != null && AudioStream.Demuxer.Type != MediaType.Video && Config.Audio.Enabled)
        {
            if (timestamp == -1) timestamp = VideoDemuxer.CurTime;
            if (CanInfo) Log.Info($"Resync audio to {TicksToTime(timestamp)}");

            SeekAudio(timestamp / 10000);
            if (isRunning)
            {
                AudioDemuxer.Start();
                AudioDecoder.Start();
            }
        }

        //for (int i = 0; i < subNum; i++)
        //{
        //    if (SubtitlesStreams[i] != null && SubtitlesStreams[i].Demuxer.Type != MediaType.Video && Config.Subtitles.Enabled)
        //    {
        //        if (timestamp == -1)
        //            timestamp = VideoDemuxer.CurTime;
        //        if (CanInfo)
        //            Log.Info($"Resync subs:{i} to {TicksToTime(timestamp)}");

        //        SeekSubtitles(i, timestamp / 10000, false);
        //        if (isRunning)
        //        {
        //            SubtitlesDemuxers[i].Start();
        //            SubtitlesDecoders[i].Start();
        //        }
        //    }
        //}

        if (DataStream != null && Config.Data.Enabled) // Should check if it actually an external (not embedded) stream DataStream.Demuxer.Type != MediaType.Video ?
        {
            if (timestamp == -1)
                timestamp = VideoDemuxer.CurTime;
            if (CanInfo)
                Log.Info($"Resync data to {TicksToTime(timestamp)}");

            SeekData(timestamp / 10000);
            if (isRunning)
            {
                DataDemuxer.Start();
                DataDecoder.Start();
            }
        }

        RequiresResync = false;
    }

    //public void ResyncSubtitles(long timestamp = -1)
    //{
    //    if (SubtitlesStream != null && Config.Subtitles.Enabled)
    //    {
    //        if (timestamp == -1) timestamp = VideoDemuxer.CurTime;
    //        if (CanInfo) Log.Info($"Resync subs to {TicksToTime(timestamp)}");

    //        if (SubtitlesStream.Demuxer.Type != MediaType.Video)
    //            SeekSubtitles(timestamp / 10000);
    //        else

    //        if (VideoDemuxer.IsRunning)
    //        {
    //            SubtitlesDemuxer.Start();
    //            SubtitlesDecoder.Start();
    //        }
    //    }
    //}
    public void Flush()
    {
        VideoDemuxer.DisposePackets();
        AudioDemuxer.DisposePackets();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDemuxers[i].DisposePackets();
        }
        DataDemuxer.DisposePackets();

        VideoDecoder.Flush();
        AudioDecoder.Flush();
        for (int i = 0; i < subNum; i++)
        {
            SubtitlesDecoders[i].Flush();
        }
        DataDecoder.Flush();
    }

    // !!! NEEDS RECODING
    public long GetVideoFrame(long timestamp = -1)
    {
        // TBR: Between seek and GetVideoFrame lockCodecCtx is lost and if VideoDecoder is running will already have decoded some frames (Currently ensure you pause VideDecoder before seek)

        int ret;
        int allowedErrors = Config.Decoder.MaxErrors;
        AVPacket* packet;

        lock (VideoDemuxer.lockFmtCtx)
        lock (VideoDecoder.lockCodecCtx)
        while (VideoDemuxer.VideoStream != null && !Interrupt)
        {
            if (VideoDemuxer.VideoPackets.IsEmpty)
            {
                packet = av_packet_alloc();
                VideoDemuxer.Interrupter.ReadRequest();
                ret = av_read_frame(VideoDemuxer.FormatContext, packet);
                if (ret != 0)
                {
                    av_packet_free(&packet);
                    return -1;
                }
            }
            else
                packet = VideoDemuxer.VideoPackets.Dequeue();

            if (!VideoDemuxer.EnabledStreams.Contains(packet->stream_index)) { av_packet_free(&packet); continue; }

            if (CanTrace)
            {
                var stream = VideoDemuxer.AVStreamToStream[packet->stream_index];
                long dts = packet->dts == AV_NOPTS_VALUE ? -1 : (long)(packet->dts * stream.Timebase);
                long pts = packet->pts == AV_NOPTS_VALUE ? -1 : (long)(packet->pts * stream.Timebase);
                Log.Trace($"[{stream.Type}] DTS: {(dts == -1 ? "-" : TicksToTime(dts))} PTS: {(pts == -1 ? "-" : TicksToTime(pts))} | FLPTS: {(pts == -1 ? "-" : TicksToTime(pts - VideoDemuxer.StartTime))} | CurTime: {TicksToTime(VideoDemuxer.CurTime)} | Buffered: {TicksToTime(VideoDemuxer.BufferedDuration)}");
            }

            var codecType = VideoDemuxer.FormatContext->streams[packet->stream_index]->codecpar->codec_type;

            if (codecType == AVMediaType.Video && VideoDecoder.keyPacketRequired)
            {
                if (packet->flags.HasFlag(PktFlags.Key))
                    VideoDecoder.keyPacketRequired = false;
                else
                {
                    if (CanWarn) Log.Warn("Ignoring non-key packet");
                    av_packet_free(&packet);
                    continue;
                }
            }

            if (VideoDemuxer.IsHLSLive)
                VideoDemuxer.UpdateHLSTime();

            switch (codecType)
            {
                case AVMediaType.Audio:
                    if (timestamp == -1 || (long)(packet->pts * AudioStream.Timebase) - VideoDemuxer.StartTime + (VideoStream.FrameDuration / 2) > timestamp)
                        VideoDemuxer.AudioPackets.Enqueue(packet);
                    else
                        av_packet_free(&packet);

                    continue;

                case AVMediaType.Subtitle:
                    for (int i = 0; i < subNum; i++)
                    {
                        if (SubtitlesStreams[i]?.StreamIndex != packet->stream_index)
                        {
                            continue;
                        }

                        if (timestamp == -1 ||
                            (long)(packet->pts * SubtitlesStreams[i].Timebase) - VideoDemuxer.StartTime +
                            (VideoStream.FrameDuration / 2) > timestamp)
                        {
                            // Clone packets to support simultaneous display of the same subtitle
                            VideoDemuxer.SubtitlesPackets[i].Enqueue(av_packet_clone(packet));
                        }
                    }

                    // cloned, so free packet
                    av_packet_free(&packet);

                    continue;

                case AVMediaType.Data: // this should catch the data stream packets until we have a valid vidoe keyframe (it should fill the pts if NOPTS with lastVideoPacketPts similarly to the demuxer)
                    if ((timestamp == -1 && VideoDecoder.StartTime != NoTs) || (long)(packet->pts * DataStream.Timebase) - VideoDemuxer.StartTime + (VideoStream.FrameDuration / 2) > timestamp)
                        VideoDemuxer.DataPackets.Enqueue(packet);

                    packet = av_packet_alloc();

                    continue;

                case AVMediaType.Video:
                    ret = avcodec_send_packet(VideoDecoder.CodecCtx, packet);

                    if (VideoDecoder.swFallback)
                    {
                        VideoDecoder.SWFallback();
                        ret = avcodec_send_packet(VideoDecoder.CodecCtx, packet);
                    }

                    av_packet_free(&packet);

                    if (ret != 0)
                    {
                        allowedErrors--;
                        if (CanWarn) Log.Warn($"{FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})");

                        if (allowedErrors == 0) { Log.Error("Too many errors!");  return -1; }

                        continue;
                    }

                    //VideoDemuxer.UpdateCurTime();

                    var frame = av_frame_alloc();
                    while (VideoDemuxer.VideoStream != null && !Interrupt)
                    {
                        ret = avcodec_receive_frame(VideoDecoder.CodecCtx, frame);
                        if (ret != 0) { av_frame_unref(frame); break; }

                        if (frame->best_effort_timestamp != AV_NOPTS_VALUE)
                            frame->pts = frame->best_effort_timestamp;
                        else if (frame->pts == AV_NOPTS_VALUE)
                        {
                            if (!VideoStream.FixTimestamps)
                            {
                                av_frame_unref(frame);
                                continue;
                            }

                            frame->pts = VideoDecoder.lastFixedPts + VideoStream.StartTimePts;
                            VideoDecoder.lastFixedPts += av_rescale_q(VideoStream.FrameDuration / 10, Engine.FFmpeg.AV_TIMEBASE_Q, VideoStream.AVStream->time_base);
                        }

                        if (!VideoDecoder.filledFromCodec)
                        {
                            ret = VideoDecoder.FillFromCodec(frame);

                            if (ret == -1234)
                            {
                                av_frame_free(&frame);
                                return -1;
                            }
                        }

                        // Accurate seek with +- half frame distance
                        if (timestamp != -1 && (long)(frame->pts * VideoStream.Timebase) - VideoDemuxer.StartTime + (VideoStream.FrameDuration / 2) < timestamp)
                        {
                            av_frame_unref(frame);
                            continue;
                        }

                        //if (CanInfo) Info($"Asked for {Utils.TicksToTime(timestamp)} and got {Utils.TicksToTime((long)(frame->pts * VideoStream.Timebase) - VideoDemuxer.StartTime)} | Diff {Utils.TicksToTime(timestamp - ((long)(frame->pts * VideoStream.Timebase) - VideoDemuxer.StartTime))}");
                        VideoDecoder.StartTime = (long)(frame->pts * VideoStream.Timebase) - VideoDemuxer.StartTime;

                        var mFrame = VideoDecoder.Renderer.FillPlanes(frame);
                        if (mFrame != null) VideoDecoder.Frames.Enqueue(mFrame);

                        do
                        {
                            ret = avcodec_receive_frame(VideoDecoder.CodecCtx, frame);
                            if (ret != 0) break;
                            mFrame = VideoDecoder.Renderer.FillPlanes(frame);
                            if (mFrame != null) VideoDecoder.Frames.Enqueue(mFrame);
                        } while (!VideoDemuxer.Disposed && !Interrupt);

                        av_frame_free(&frame);
                        return mFrame.timestamp;
                    }

                    av_frame_free(&frame);

                    break; // Switch break

                default:
                    av_packet_free(&packet);
                    continue;

            } // Switch

        } // While

        return -1;
    }
    public new void Dispose()
    {
        shouldDispose = true;
        Stop();
        Interrupt = true;
        VideoDecoder.DestroyRenderer();
        base.Dispose();
    }

    public void PrintStats()
    {
        string dump = "\r\n-===== Streams / Packets / Frames =====-\r\n";
        dump += $"\r\n AudioPackets      ({VideoDemuxer.AudioStreams.Count}): {VideoDemuxer.AudioPackets.Count}";
        dump += $"\r\n VideoPackets      ({VideoDemuxer.VideoStreams.Count}): {VideoDemuxer.VideoPackets.Count}";
        for (int i = 0; i < subNum; i++)
        {
            dump += $"\r\n SubtitlesPackets{i+1}  ({VideoDemuxer.SubtitlesStreamsAll.Count}): {VideoDemuxer.SubtitlesPackets[i].Count}";
        }

        dump += $"\r\n AudioPackets      ({AudioDemuxer.AudioStreams.Count}): {AudioDemuxer.AudioPackets.Count} (AudioDemuxer)";

        for (int i = 0; i < subNum; i++)
        {
            dump += $"\r\n SubtitlesPackets{i+1}  ({SubtitlesDemuxers[i].SubtitlesStreamsAll.Count}): {SubtitlesDemuxers[i].SubtitlesPackets[0].Count} (SubtitlesDemuxer)";
        }

        dump += $"\r\n Video Frames         : {VideoDecoder.Frames.Count}";
        dump += $"\r\n Audio Frames         : {AudioDecoder.Frames.Count}";
        for (int i = 0; i < subNum; i++)
        {
            dump += $"\r\n Subtitles Frames{i+1}     : {SubtitlesDecoders[i].Frames.Count}";
        }

        if (CanInfo) Log.Info(dump);
    }

    #region Recorder
    Remuxer Recorder;
    public event EventHandler RecordingCompleted;
    public bool IsRecording => VideoDecoder.isRecording || AudioDecoder.isRecording;
    int oldMaxAudioFrames;
    bool recHasVideo;
    public void StartRecording(ref string filename, bool useRecommendedExtension = true)
    {
        if (IsRecording) StopRecording();

        oldMaxAudioFrames = -1;
        recHasVideo = false;

        if (CanInfo) Log.Info("Record Start");

        recHasVideo = !VideoDecoder.Disposed && VideoDecoder.Stream != null;

        if (useRecommendedExtension)
            filename = $"{filename}.{(recHasVideo ? VideoDecoder.Stream.Demuxer.Extension : AudioDecoder.Stream.Demuxer.Extension)}";

        Recorder.Open(filename);

        bool failed;

        if (recHasVideo)
        {
            failed = Recorder.AddStream(VideoDecoder.Stream.AVStream) != 0;
            if (CanInfo) Log.Info(failed ? "Failed to add video stream" : "Video stream added to the recorder");
        }

        if (!AudioDecoder.Disposed && AudioDecoder.Stream != null)
        {
            failed = Recorder.AddStream(AudioDecoder.Stream.AVStream, !AudioDecoder.OnVideoDemuxer) != 0;
            if (CanInfo) Log.Info(failed ? "Failed to add audio stream" : "Audio stream added to the recorder");
        }

        if (!Recorder.HasStreams || Recorder.WriteHeader() != 0) return; //throw new Exception("Invalid remuxer configuration");

        // Check also buffering and possible Diff of first audio/video timestamp to remuxer to ensure sync between each other (shouldn't be more than 30-50ms)
        oldMaxAudioFrames = Config.Decoder.MaxAudioFrames;
        //long timestamp = Math.Max(VideoDemuxer.CurTime + VideoDemuxer.BufferedDuration, AudioDemuxer.CurTime + AudioDemuxer.BufferedDuration) + 1500 * 10000;
        Config.Decoder.MaxAudioFrames = Config.Decoder.MaxVideoFrames;

        VideoDecoder.StartRecording(Recorder);
        AudioDecoder.StartRecording(Recorder);
    }
    public void StopRecording()
    {
        if (oldMaxAudioFrames != -1) Config.Decoder.MaxAudioFrames = oldMaxAudioFrames;

        VideoDecoder.StopRecording();
        AudioDecoder.StopRecording();
        Recorder.Dispose();
        oldMaxAudioFrames = -1;
        if (CanInfo) Log.Info("Record Completed");
    }
    internal void RecordCompleted(MediaType type)
    {
        if (!recHasVideo || (recHasVideo && type == MediaType.Video))
        {
            StopRecording();
            RecordingCompleted?.Invoke(this, new EventArgs());
        }
    }
    #endregion
}

using System.IO;
using System.Threading;
using System.Threading.Tasks;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaRemuxer;

using static FlyleafLib.Logger;

/* TODO
 * Don't let audio go further than video (because of large duration without video)?
 *
 */

namespace FlyleafLib.MediaFramework.MediaContext;

/// <summary>
/// Downloads or remuxes to different format any ffmpeg valid input url
/// </summary>
public unsafe class Downloader : RunThreadBase
{
    /// <summary>
    /// The backend demuxer. Access this to enable the required streams for downloading
    /// </summary>
    //public Demuxer      Demuxer             { get; private set; }

    public DecoderContext DecCtx { get; private set; }
    internal Demuxer AudioDemuxer => DecCtx.AudioDemuxer;

    /// <summary>
    /// The backend remuxer. Normally you shouldn't access this
    /// </summary>
    public Remuxer      Remuxer             { get; private set; }

    /// <summary>
    /// The current timestamp of the frame starting from 0 (Ticks)
    /// </summary>
    public long         CurTime             { get => _CurTime; private set => Set(ref _CurTime,  value); }
    long _CurTime;

    /// <summary>
    /// The total duration of the input (Ticks)
    /// </summary>
    public long         Duration            { get => _Duration; private set => Set(ref _Duration, value); }
    long _Duration;

    /// <summary>
    /// The percentage of the current download process (0 for live streams)
    /// </summary>
    public double       DownloadPercentage  { get => _DownloadPercentage;     set => Set(ref _DownloadPercentage,  value); }
    double _DownloadPercentage;
    double downPercentageFactor;

    public Downloader(Config config, int uniqueId = -1) : base(uniqueId)
    {
        DecCtx = new DecoderContext(config, UniqueId, false);
        DecCtx.AudioDemuxer.Config = config.Demuxer.Clone(); // We change buffer duration and we need to avoid changing video demuxer's config

        //DecCtx.VideoInputOpened += (o, e) => { if (!e.Success) OnDownloadCompleted(false); };
        Remuxer = new Remuxer(UniqueId);
        threadName = "Downloader";
    }

    /// <summary>
    /// Fires on download completed or failed
    /// </summary>
    public event EventHandler<bool> DownloadCompleted;
    protected virtual void OnDownloadCompleted(bool success)
        => Task.Run(() => { Dispose(); DownloadCompleted?.Invoke(this, success); });

    /// <summary>
    /// Opens a new media file (audio/video) and prepares it for download (blocking)
    /// </summary>
    /// <param name="url">Media file's url</param>
    /// <param name="defaultPlaylistItem">Whether to open the default input (in case of multiple inputs eg. from bitswarm/youtube-dl, you might want to choose yours)</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <returns></returns>
    public string Open(string url, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true) => Open((object) url, defaultPlaylistItem, defaultVideo, defaultAudio);

    /// <summary>
    /// Opens a new media file (audio/video) and prepares it for download (blocking)
    /// </summary>
    /// <param name="stream">Media Stream</param>
    /// <param name="defaultPlaylistItem">Whether to open the default input (in case of multiple inputs eg. from bitswarm/youtube-dl, you might want to choose yours)</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <returns></returns>
    public string Open(Stream stream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true) => Open((object) stream, defaultPlaylistItem, defaultVideo, defaultAudio);

    internal string Open(object url, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true)
    {
        lock (lockActions)
        {
            Dispose();

            Disposed= false;
            Status  = Status.Opening;
            var ret = DecCtx.Open(url, defaultPlaylistItem, defaultVideo, defaultAudio, false);
            if (ret != null && ret.Error != null) return ret.Error;

            CurTime = 0;
            DownloadPercentage = 0;

            var Demuxer = !DecCtx.VideoDemuxer.Disposed ? DecCtx.VideoDemuxer : DecCtx.AudioDemuxer;
            Duration = Demuxer.IsLive ? 0 : Demuxer.Duration;
            downPercentageFactor = Duration / 100.0;

            return null;
        }
    }

    /// <summary>
    /// Downloads the currently configured AVS streams
    /// </summary>
    /// <param name="filename">The filename for the downloaded video. The file extension will let the demuxer to choose the output format (eg. mp4). If you useRecommendedExtension will be updated with the extension.</param>
    /// <param name="useRecommendedExtension">Will try to match the output container with the input container</param>
    public void Download(ref string filename, bool useRecommendedExtension = true)
    {
        lock (lockActions)
        {
            if (Status != Status.Opening || Disposed)
                { OnDownloadCompleted(false); return; }

            if (useRecommendedExtension)
                filename = $"{filename}.{(!DecCtx.VideoDemuxer.Disposed ? DecCtx.VideoDemuxer.Extension : DecCtx.AudioDemuxer.Extension)}";

            int ret = Remuxer.Open(filename);
            if (ret != 0)
                { OnDownloadCompleted(false); return; }

            AddStreams(DecCtx.VideoDemuxer);
            AddStreams(DecCtx.AudioDemuxer);

            if (!Remuxer.HasStreams || Remuxer.WriteHeader() != 0)
                { OnDownloadCompleted(false); return; }

            Start();
        }
    }

    private void AddStreams(Demuxer demuxer)
    {
        for(int i=0; i<demuxer.EnabledStreams.Count; i++)
            if (Remuxer.AddStream(demuxer.AVStreamToStream[demuxer.EnabledStreams[i]].AVStream, demuxer.Type == MediaType.Audio) != 0)
                Log.Warn($"Failed to add stream {demuxer.AVStreamToStream[demuxer.EnabledStreams[i]].Type} {demuxer.AVStreamToStream[demuxer.EnabledStreams[i]].StreamIndex}");
    }

    /// <summary>
    /// Stops and disposes the downloader
    /// </summary>
    public void Dispose()
    {
        if (Disposed) return;

        lock (lockActions)
        {
            if (Disposed) return;

            Stop();

            DecCtx.Dispose();
            Remuxer.Dispose();

            Status = Status.Stopped;
            Disposed = true;
        }
    }

    protected override void RunInternal()
    {
        if (!Remuxer.HasStreams) { OnDownloadCompleted(false); return; }

        // Don't allow audio to change our duration without video (TBR: requires timestamp of videodemuxer to wait)
        long resetBufferDuration = -1;
        bool hasAVDemuxers = !DecCtx.VideoDemuxer.Disposed && !DecCtx.AudioDemuxer.Disposed;
        if (hasAVDemuxers)
        {
            resetBufferDuration = DecCtx.AudioDemuxer.Config.BufferDuration;
            DecCtx.AudioDemuxer.Config.BufferDuration = 100 * 10000;
        }

        DecCtx.Start();

        var Demuxer = !DecCtx.VideoDemuxer.Disposed ? DecCtx.VideoDemuxer : DecCtx.AudioDemuxer;
        long startTime = Demuxer.hlsCtx == null ? Demuxer.StartTime : Demuxer.hlsCtx->first_timestamp * 10;
        Duration = Demuxer.IsLive ? 0 : Demuxer.Duration;
        downPercentageFactor = Duration / 100.0;

        long startedAt = DateTime.UtcNow.Ticks;
        long secondTicks = startedAt;
        bool isAudioDemuxer = DecCtx.VideoDemuxer.Disposed && !DecCtx.AudioDemuxer.Disposed;
        IntPtr pktPtr = IntPtr.Zero;
        AVPacket* packet = null;
        AVPacket* packet2 = null;

        do
        {
            if ((Demuxer.Packets.Count == 0 && AudioDemuxer.Packets.Count == 0) || (hasAVDemuxers && (Demuxer.Packets.Count == 0 || AudioDemuxer.Packets.Count == 0)))
            {
                lock (lockStatus)
                    if (Status == Status.Running) Status = Status.QueueEmpty;

                while (Demuxer.Packets.Count == 0 && Status == Status.QueueEmpty)
                {
                    if (Demuxer.Status == Status.Ended)
                    {
                        Status = Status.Ended;
                        if (Demuxer.Interrupter.Interrupted == 0)
                        {
                            CurTime = _Duration;
                            DownloadPercentage = 100;
                        }
                        break;
                    }
                    else if (!Demuxer.IsRunning)
                    {
                        if (CanDebug) Log.Debug($"Demuxer is not running [Demuxer Status: {Demuxer.Status}]");

                        lock (Demuxer.lockStatus)
                        lock (lockStatus)
                        {
                            if (Demuxer.Status == Status.Pausing || Demuxer.Status == Status.Paused)
                                Status = Status.Pausing;
                            else if (Demuxer.Status != Status.Ended)
                                Status = Status.Stopping;
                            else
                                continue;
                        }

                        break;
                    }

                    Thread.Sleep(20);
                }

                if (hasAVDemuxers && Status == Status.QueueEmpty && AudioDemuxer.Packets.Count == 0)
                {
                    while (AudioDemuxer.Packets.Count == 0 && Status == Status.QueueEmpty && AudioDemuxer.IsRunning)
                        Thread.Sleep(20);
                }

                lock (lockStatus)
                {
                    if (Status != Status.QueueEmpty) break;
                    Status = Status.Running;
                }
            }

            if (hasAVDemuxers)
            {
                if (AudioDemuxer.Packets.Count == 0)
                {
                    packet = Demuxer.Packets.Dequeue();
                    isAudioDemuxer = false;
                }
                else if (Demuxer.Packets.Count == 0)
                {
                    packet = AudioDemuxer.Packets.Dequeue();
                    isAudioDemuxer = true;
                }
                else
                {
                    packet  = Demuxer.Packets.Peek();
                    packet2 = AudioDemuxer.Packets.Peek();

                    long ts1 = (long) ((packet->dts * Demuxer.AVStreamToStream[packet->stream_index].Timebase) - Demuxer.StartTime);
                    long ts2 = (long) ((packet2->dts * AudioDemuxer.AVStreamToStream[packet2->stream_index].Timebase) - AudioDemuxer.StartTime);

                    if (ts2 <= ts1)
                    {
                        AudioDemuxer.Packets.Dequeue();
                        isAudioDemuxer = true;
                        packet = packet2;
                    }
                    else
                    {
                        Demuxer.Packets.Dequeue();
                        isAudioDemuxer = false;
                    }

                    //Log($"[{isAudioDemuxer}] {Utils.TicksToTime(ts1)} | {Utils.TicksToTime(ts2)}");
                }
            }
            else
            {
                packet = Demuxer.Packets.Dequeue();
            }

            long curDT = DateTime.UtcNow.Ticks;
            if (curDT - secondTicks > 1000 * 10000 && (!isAudioDemuxer || DecCtx.VideoDemuxer.Disposed))
            {
                secondTicks = curDT;

                CurTime = Demuxer.hlsCtx != null
                    ? (long) ((packet->dts * Demuxer.AVStreamToStream[packet->stream_index].Timebase) - startTime)
                    : Demuxer.CurTime + Demuxer.BufferedDuration;

                if (_Duration > 0) DownloadPercentage = CurTime / downPercentageFactor;
            }

            Remuxer.Write(packet, isAudioDemuxer);

        } while (Status == Status.Running);

        if (resetBufferDuration != -1) DecCtx.AudioDemuxer.Config.BufferDuration = resetBufferDuration;

        if (Status != Status.Pausing && Status != Status.Paused)
            OnDownloadCompleted(Remuxer.WriteTrailer() == 0);
        else
            Demuxer.Pause();
    }
}

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.Plugins;

public class PluginHandler
{
    #region Properties
    public Config                   Config                          { get; private set; }
    public bool                     Interrupt                       { get; set; }
    public IOpen                    OpenedPlugin                    { get; private set; }
    public IOpenSubtitles           OpenedSubtitlesPlugin           { get; private set; }
    public long                     OpenCounter                     { get; internal set; }
    public long                     OpenItemCounter                 { get; internal set; }
    public Playlist                 Playlist                        { get; set; }
    public int                      UniqueId                        { get; set; }

    public Dictionary<string, PluginBase>
                                    Plugins                         { get; private set; }
    public Dictionary<string, IOpen>
                                    PluginsOpen                     { get; private set; }
    public Dictionary<string, IOpenSubtitles>
                                    PluginsOpenSubtitles            { get; private set; }

    public Dictionary<string, IScrapeItem>
                                    PluginsScrapeItem               { get; private set; }

    public Dictionary<string, ISuggestPlaylistItem>
                                    PluginsSuggestItem              { get; private set; }

    public Dictionary<string, ISuggestAudioStream>
                                    PluginsSuggestAudioStream       { get; private set; }
    public Dictionary<string, ISuggestVideoStream>
                                    PluginsSuggestVideoStream       { get; private set; }
    public Dictionary<string, ISuggestExternalAudio>
                                    PluginsSuggestExternalAudio     { get; private set; }
    public Dictionary<string, ISuggestExternalVideo>
                                    PluginsSuggestExternalVideo     { get; private set; }

    public Dictionary<string, ISuggestSubtitlesStream>
                                    PluginsSuggestSubtitlesStream   { get; private set; }
    public Dictionary<string, ISuggestSubtitles>
                                    PluginsSuggestSubtitles         { get; private set; }
    public Dictionary<string, ISuggestBestExternalSubtitles>
                                    PluginsSuggestBestExternalSubtitles
                                                                    { get; private set; }

    public Dictionary<string, IDownloadSubtitles>
                                    PluginsDownloadSubtitles        { get; private set; }

    public Dictionary<string, ISearchLocalSubtitles>
                                    PluginsSearchLocalSubtitles     { get; private set; }
    public Dictionary<string, ISearchOnlineSubtitles>
                                    PluginsSearchOnlineSubtitles    { get; private set; }
    #endregion

    #region Initialize
    LogHandler Log;
    public PluginHandler(Config config, int uniqueId = -1)
    {
        Config = config;
        UniqueId= uniqueId == -1 ? Utils.GetUniqueId() : uniqueId;
        Playlist = new Playlist(UniqueId);
        Log = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + " [PluginHandler ] ");
        LoadPlugins();
    }

    public static PluginBase CreatePluginInstance(PluginType type, PluginHandler handler = null)
    {
        PluginBase plugin = (PluginBase) Activator.CreateInstance(type.Type, true);
        plugin.Handler  = handler;
        plugin.Name     = type.Name;
        plugin.Type     = type.Type;
        plugin.Version  = type.Version;

        if (handler != null)
            plugin.OnLoaded();

        return plugin;
    }
    private void LoadPlugins()
    {
        Plugins = new Dictionary<string, PluginBase>();

        foreach (var type in Engine.Plugins.Types.Values)
        {
            try
            {
                var plugin = CreatePluginInstance(type, this);
                plugin.Log = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + $" [{plugin.Name,-14}] ");
                Plugins.Add(plugin.Name, plugin);
            } catch (Exception e) { Log.Error($"[Plugins] [Error] Failed to load plugin ... ({e.Message} {Utils.GetRecInnerException(e)}"); }
            }

        PluginsOpen                     = new Dictionary<string, IOpen>();
        PluginsOpenSubtitles            = new Dictionary<string, IOpenSubtitles>();
        PluginsScrapeItem               = new Dictionary<string, IScrapeItem>();

        PluginsSuggestItem              = new Dictionary<string, ISuggestPlaylistItem>();

        PluginsSuggestAudioStream       = new Dictionary<string, ISuggestAudioStream>();
        PluginsSuggestVideoStream       = new Dictionary<string, ISuggestVideoStream>();
        PluginsSuggestSubtitlesStream   = new Dictionary<string, ISuggestSubtitlesStream>();
        PluginsSuggestSubtitles         = new Dictionary<string, ISuggestSubtitles>();

        PluginsSuggestExternalAudio     = new Dictionary<string, ISuggestExternalAudio>();
        PluginsSuggestExternalVideo     = new Dictionary<string, ISuggestExternalVideo>();
        PluginsSuggestBestExternalSubtitles
                                        = new Dictionary<string, ISuggestBestExternalSubtitles>();

        PluginsSearchLocalSubtitles     = new Dictionary<string, ISearchLocalSubtitles>();
        PluginsSearchOnlineSubtitles    = new Dictionary<string, ISearchOnlineSubtitles>();
        PluginsDownloadSubtitles        = new Dictionary<string, IDownloadSubtitles>();

        foreach (var plugin in Plugins.Values)
            LoadPluginInterfaces(plugin);
    }
    private void LoadPluginInterfaces(PluginBase plugin)
    {
        if (plugin is IOpen) PluginsOpen.Add(plugin.Name, (IOpen)plugin);
        else if (plugin is IOpenSubtitles) PluginsOpenSubtitles.Add(plugin.Name, (IOpenSubtitles)plugin);

        if (plugin is IScrapeItem) PluginsScrapeItem.Add(plugin.Name, (IScrapeItem)plugin);

        if (plugin is ISuggestPlaylistItem) PluginsSuggestItem.Add(plugin.Name, (ISuggestPlaylistItem)plugin);

        if (plugin is ISuggestAudioStream) PluginsSuggestAudioStream.Add(plugin.Name, (ISuggestAudioStream)plugin);
        if (plugin is ISuggestVideoStream) PluginsSuggestVideoStream.Add(plugin.Name, (ISuggestVideoStream)plugin);
        if (plugin is ISuggestSubtitlesStream) PluginsSuggestSubtitlesStream.Add(plugin.Name, (ISuggestSubtitlesStream)plugin);
        if (plugin is ISuggestSubtitles) PluginsSuggestSubtitles.Add(plugin.Name, (ISuggestSubtitles)plugin);

        if (plugin is ISuggestExternalAudio) PluginsSuggestExternalAudio.Add(plugin.Name, (ISuggestExternalAudio)plugin);
        if (plugin is ISuggestExternalVideo) PluginsSuggestExternalVideo.Add(plugin.Name, (ISuggestExternalVideo)plugin);
        if (plugin is ISuggestBestExternalSubtitles) PluginsSuggestBestExternalSubtitles.Add(plugin.Name, (ISuggestBestExternalSubtitles)plugin);

        if (plugin is ISearchLocalSubtitles) PluginsSearchLocalSubtitles.Add(plugin.Name, (ISearchLocalSubtitles)plugin);
        if (plugin is ISearchOnlineSubtitles) PluginsSearchOnlineSubtitles.Add(plugin.Name, (ISearchOnlineSubtitles)plugin);
        if (plugin is IDownloadSubtitles) PluginsDownloadSubtitles.Add(plugin.Name, (IDownloadSubtitles)plugin);
    }
    #endregion

    #region Misc / Events
    public void OnInitializing()
    {
        OpenCounter++;
        OpenItemCounter++;
        foreach(var plugin in Plugins.Values)
            plugin.OnInitializing();
    }
    public void OnInitialized()
    {
        OpenedPlugin            = null;
        OpenedSubtitlesPlugin   = null;

        Playlist.Reset();

        foreach(var plugin in Plugins.Values)
            plugin.OnInitialized();
    }

    public void OnInitializingSwitch()
    {
        OpenItemCounter++;
        foreach(var plugin in Plugins.Values)
            plugin.OnInitializingSwitch();
    }
    public void OnInitializedSwitch()
    {
        foreach(var plugin in Plugins.Values)
            plugin.OnInitializedSwitch();
    }

    public void Dispose()
    {
        foreach(var plugin in Plugins.Values)
            plugin.Dispose();
    }
    #endregion

    #region Audio / Video
    public OpenResults Open()
    {
        long sessionId = OpenCounter;
        var plugins = PluginsOpen.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt || sessionId != OpenCounter)
                return new OpenResults("Cancelled");

            if (!plugin.CanOpen())
                continue;

            var res = plugin.Open();
            if (res == null)
                continue;

            // Currently fallback is not allowed if an error has been returned
            if (res.Error != null)
                return res;

            OpenedPlugin = plugin;
            Log.Info($"[{plugin.Name}] Open Success");

            return res;
        }

        return new OpenResults("No plugin found for the provided input");
    }
    public OpenResults OpenItem()
    {
        long sessionId = OpenItemCounter;
        var res = OpenedPlugin.OpenItem();

        res ??= new OpenResults { Error = "Cancelled" };

        if (sessionId != OpenItemCounter)
            res.Error = "Cancelled";

        if (res.Error == null)
            Log.Info($"[{OpenedPlugin.Name}] Open Item ({Playlist.Selected.Index}) Success");

        return res;
    }

    // Should only be called from opened plugin
    public void OnPlaylistCompleted()
    {
        Playlist.Completed = true;
        if (Playlist.ExpectingItems == 0)
            Playlist.ExpectingItems = Playlist.Items.Count;

        if (Playlist.Items.Count > 1)
        {
            Log.Debug("Playlist Completed");
            Playlist.UpdatePrevNextItem();
        }
    }

    public void ScrapeItem(PlaylistItem item)
    {
        var plugins = PluginsScrapeItem.Values.OrderBy(x => x.Priority);
        foreach (var plugin in plugins)
        {
            if (Interrupt)
                return;

            plugin.ScrapeItem(item);
        }
    }

    public PlaylistItem SuggestItem()
    {
        var plugins = PluginsSuggestItem.Values.OrderBy(x => x.Priority);
        foreach (var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var item = plugin.SuggestItem();
            if (item != null)
            {
                Log.Info($"SuggestItem #{item.Index} - {item.Title}");
                return item;
            }
        }

        return null;
    }

    public ExternalVideoStream SuggestExternalVideo()
    {
        var plugins = PluginsSuggestExternalVideo.Values.OrderBy(x => x.Priority);
        foreach (var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var extStream = plugin.SuggestExternalVideo();
            if (extStream != null)
            {
                Log.Info($"SuggestVideo (External) {extStream.Width} x {extStream.Height} @ {extStream.FPS}");
                Log.Debug($"SuggestVideo (External) Url: {extStream.Url}, UrlFallback: {extStream.UrlFallback}");
                return extStream;
            }
        }

        return null;
    }
    public ExternalAudioStream SuggestExternalAudio()
    {
        var plugins = PluginsSuggestExternalAudio.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var extStream = plugin.SuggestExternalAudio();
            if (extStream != null)
            {
                Log.Info($"SuggestAudio (External) {extStream.SampleRate} Hz, {extStream.Codec}");
                Log.Debug($"SuggestAudio (External) Url: {extStream.Url}, UrlFallback: {extStream.UrlFallback}");
                return extStream;
            }
        }

        return null;
    }

    public VideoStream SuggestVideo(ObservableCollection<VideoStream> streams)
    {
        if (streams == null || streams.Count == 0) return null;

        var plugins = PluginsSuggestVideoStream.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var stream = plugin.SuggestVideo(streams);
            if (stream != null) return stream;
        }

        return null;
    }
    public void SuggestVideo(out VideoStream stream, out ExternalVideoStream extStream, ObservableCollection<VideoStream> streams)
    {
        stream = null;
        extStream = null;

        if (Interrupt)
            return;

        stream = SuggestVideo(streams);
        if (stream != null)
            return;

        if (Interrupt)
            return;

        extStream = SuggestExternalVideo();
    }

    public AudioStream SuggestAudio(ObservableCollection<AudioStream> streams)
    {
        if (streams == null || streams.Count == 0) return null;

        var plugins = PluginsSuggestAudioStream.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var stream = plugin.SuggestAudio(streams);
            if (stream != null) return stream;
        }

        return null;
    }
    public void SuggestAudio(out AudioStream stream, out ExternalAudioStream extStream, ObservableCollection<AudioStream> streams)
    {
        stream = null;
        extStream = null;

        if (Interrupt)
            return;

        stream = SuggestAudio(streams);
        if (stream != null)
            return;

        if (Interrupt)
            return;

        extStream = SuggestExternalAudio();
    }
    #endregion

    #region Subtitles
    public OpenSubtitlesResults OpenSubtitles(string url)
    {
        var plugins = PluginsOpenSubtitles.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            var res = plugin.Open(url);
            if (res == null)
                continue;

            if (res.Error != null)
                return res;

            OpenedSubtitlesPlugin = plugin;
            Log.Info($"[{plugin.Name}] Open Subtitles Success");

            return res;
        }

        return null;
    }

    public bool SearchLocalSubtitles()
    {
        if (!Playlist.Selected.SearchedLocal && Config.Subtitles.SearchLocal && (Config.Subtitles.SearchLocalOnInputType == null || Config.Subtitles.SearchLocalOnInputType.Count == 0 || Config.Subtitles.SearchLocalOnInputType.Contains(Playlist.InputType)))
        {
            Log.Debug("[Subtitles] Searching Local");
            var plugins = PluginsSearchLocalSubtitles.Values.OrderBy(x => x.Priority);
            foreach(var plugin in plugins)
            {
                if (Interrupt)
                    return false;

                plugin.SearchLocalSubtitles();
            }

            Playlist.Selected.SearchedLocal = true;

            return true;
        }

        return false;
    }
    public void SearchOnlineSubtitles()
    {
        if (!Playlist.Selected.SearchedOnline && Config.Subtitles.SearchOnline && (Config.Subtitles.SearchOnlineOnInputType == null || Config.Subtitles.SearchOnlineOnInputType.Count == 0 || Config.Subtitles.SearchOnlineOnInputType.Contains(Playlist.InputType)))
        {
            Log.Debug("[Subtitles] Searching Online");
            var plugins = PluginsSearchOnlineSubtitles.Values.OrderBy(x => x.Priority);
            foreach(var plugin in plugins)
            {
                if (Interrupt)
                    return;

                plugin.SearchOnlineSubtitles();
            }

            Playlist.Selected.SearchedOnline = true;
        }
    }
    public bool DownloadSubtitles(ExternalSubtitlesStream extStream)
    {
        bool res = false;

        var plugins = PluginsDownloadSubtitles.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
            if (res = plugin.DownloadSubtitles(extStream))
            {
                extStream.Downloaded = true;
                return res;
            }

        return res;
    }

    public ExternalSubtitlesStream SuggestBestExternalSubtitles()
    {
        var plugins = PluginsSuggestBestExternalSubtitles.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var extStream = plugin.SuggestBestExternalSubtitles();
            if (extStream != null)
                return extStream;
        }

        return null;
    }
    public void SuggestSubtitles(out SubtitlesStream stream, out ExternalSubtitlesStream extStream)
    {
        stream = null;
        extStream = null;

        var plugins = PluginsSuggestSubtitles.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return;

            plugin.SuggestSubtitles(out stream, out extStream);
            if (stream != null || extStream != null)
                return;
        }
    }
    public SubtitlesStream SuggestSubtitles(ObservableCollection<SubtitlesStream> streams, List<Language> langs)
    {
        if (streams == null || streams.Count == 0) return null;

        var plugins = PluginsSuggestSubtitlesStream.Values.OrderBy(x => x.Priority);
        foreach(var plugin in plugins)
        {
            if (Interrupt)
                return null;

            var stream = plugin.SuggestSubtitles(streams, langs);
            if (stream != null)
                return stream;
        }

        return null;
    }
    #endregion

    #region Data
    public void SuggestData(out DataStream stream, ObservableCollection<DataStream> streams)
    {
        stream = null;

        if (Interrupt)
            return;

        stream = streams.FirstOrDefault();
    }
    #endregion
}

using System.Collections.ObjectModel;
using System.IO;

using FlyleafLib.MediaFramework.MediaContext;

using static FlyleafLib.Utils;

namespace FlyleafLib.MediaFramework.MediaPlaylist;

public class Playlist : NotifyPropertyChanged
{
    /// <summary>
    /// Url provided by user
    /// </summary>
    public string       Url             { get => _Url;   set { string fixedUrl = FixFileUrl(value); SetUI(ref _Url, fixedUrl); } }
    string _Url;

    /// <summary>
    /// IOStream provided by user
    /// </summary>
    public Stream       IOStream        { get; set; }

    /// <summary>
    /// Playlist's folder base which can be used to save related files
    /// </summary>
    public string       FolderBase      { get; set; }

    /// <summary>
    /// Playlist's title
    /// </summary>
    public string       Title           { get => _Title; set => SetUI(ref _Title, value); }
    string _Title;

    public int          ExpectingItems  { get => _ExpectingItems; set => SetUI(ref _ExpectingItems, value); }
    int _ExpectingItems;

    public bool         Completed       { get; set; }

    /// <summary>
    /// Playlist's opened/selected item
    /// </summary>
    public PlaylistItem Selected        { get => _Selected; internal set { SetUI(ref _Selected, value); UpdatePrevNextItem(); } }
    PlaylistItem _Selected;

    public PlaylistItem NextItem        { get => _NextItem; internal set => SetUI(ref _NextItem, value); }
    PlaylistItem _NextItem;

    public PlaylistItem PrevItem        { get => _PrevItem; internal set => SetUI(ref _PrevItem, value); }
    PlaylistItem _PrevItem;

    internal void UpdatePrevNextItem()
    {
        if (Selected == null)
        {
            PrevItem = NextItem = null;
            return;
        }

        for (int i=0; i < Items.Count; i++)
        {
            if (Items[i] == Selected)
            {
                PrevItem = i > 0 ? Items[i - 1] : null;
                NextItem = i < Items.Count - 1 ? Items[i + 1] : null;

                return;
            }
        }
    }

    /// <summary>
    /// Type of the provided input (such as File, UNC, Torrent, Web, etc.)
    /// </summary>
    public InputType    InputType       { get; set; }

    // TODO: MediaType (Music/MusicClip/Movie/TVShow/etc.) probably should go per Playlist Item

    public ObservableCollection<PlaylistItem>
                        Items           { get; set; } = new ObservableCollection<PlaylistItem>();
    object lockItems = new();

    long openCounter;
    //long openItemCounter;
    internal DecoderContext decoder;
    LogHandler Log;

    public Playlist(int uniqueId)
    {
        Log = new LogHandler(("[#" + uniqueId + "]").PadRight(8, ' ') + " [Playlist] ");
        UIInvokeIfRequired(() => System.Windows.Data.BindingOperations.EnableCollectionSynchronization(Items, lockItems));
    }

    public void Reset()
    {
        openCounter = decoder.OpenCounter;

        lock (lockItems)
            Items.Clear();

        bool noupdate = _Url == null && _Title == null && _Selected == null;

        _Url        = null;
        _Title      = null;
        _Selected   = null;
        PrevItem    = null;
        NextItem    = null;
        IOStream    = null;
        FolderBase  = null;
        Completed   = false;
        ExpectingItems = 0;

        InputType   = InputType.Unknown;

        if (!noupdate)
            UI(() =>
            {
                Raise(nameof(Url));
                Raise(nameof(Title));
                Raise(nameof(Selected));
            });
    }

    public void AddItem(PlaylistItem item, string pluginName, object tag = null)
    {
        if (openCounter != decoder.OpenCounter)
        {
            Log.Debug("AddItem Cancelled");
            return;
        }

        lock (lockItems)
        {
            Items.Add(item);
            Items[^1].Index = Items.Count - 1;

            UpdatePrevNextItem();

            if (tag != null)
                item.AddTag(tag, pluginName);
        };

        decoder.ScrapeItem(item);

        UIInvokeIfRequired(() =>
        {
            System.Windows.Data.BindingOperations.EnableCollectionSynchronization(item.ExternalAudioStreams, item.lockExternalStreams);
            System.Windows.Data.BindingOperations.EnableCollectionSynchronization(item.ExternalVideoStreams, item.lockExternalStreams);
            System.Windows.Data.BindingOperations.EnableCollectionSynchronization(item.ExternalSubtitlesStreamsAll, item.lockExternalStreams);
        });
    }
}

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;
using FlyleafLib.MediaPlayer;
using static FlyleafLib.Logger;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaFramework.MediaContext;

public partial class DecoderContext
{
    #region Events
    public event EventHandler<OpenCompletedArgs>                        OpenCompleted;
    public event EventHandler<OpenSessionCompletedArgs>                 OpenSessionCompleted;
    public event EventHandler<OpenSubtitlesCompletedArgs>               OpenSubtitlesCompleted;
    public event EventHandler<OpenPlaylistItemCompletedArgs>            OpenPlaylistItemCompleted;

    public event EventHandler<OpenAudioStreamCompletedArgs>             OpenAudioStreamCompleted;
    public event EventHandler<OpenVideoStreamCompletedArgs>             OpenVideoStreamCompleted;
    public event EventHandler<OpenSubtitlesStreamCompletedArgs>         OpenSubtitlesStreamCompleted;
    public event EventHandler<OpenDataStreamCompletedArgs>              OpenDataStreamCompleted;

    public event EventHandler<OpenExternalAudioStreamCompletedArgs>     OpenExternalAudioStreamCompleted;
    public event EventHandler<OpenExternalVideoStreamCompletedArgs>     OpenExternalVideoStreamCompleted;
    public event EventHandler<OpenExternalSubtitlesStreamCompletedArgs> OpenExternalSubtitlesStreamCompleted;

    public class OpenCompletedArgs
    {
        public string       Url;
        public Stream       IOStream;
        public string       Error;
        public bool         Success => Error == null;
        public OpenCompletedArgs(string url = null, Stream iostream = null, string error = null) { Url = url; IOStream = iostream; Error = error; }
    }
    public class OpenSubtitlesCompletedArgs
    {
        public string       Url;
        public string       Error;
        public bool         Success => Error == null;
        public OpenSubtitlesCompletedArgs(string url = null, string error = null) { Url = url; Error = error; }
    }
    public class OpenSessionCompletedArgs
    {
        public Session      Session;
        public string       Error;
        public bool         Success => Error == null;
        public OpenSessionCompletedArgs(Session session = null, string error = null) { Session = session; Error = error; }
    }
    public class OpenPlaylistItemCompletedArgs
    {
        public PlaylistItem Item;
        public PlaylistItem OldItem;
        public string       Error;
        public bool         Success => Error == null;
        public OpenPlaylistItemCompletedArgs(PlaylistItem item = null, PlaylistItem oldItem = null, string error = null) {  Item = item; OldItem = oldItem; Error = error; }
    }
    public class StreamOpenedArgs
    {
        public StreamBase   Stream;
        public StreamBase   OldStream;
        public string       Error;
        public bool         Success => Error == null;
        public StreamOpenedArgs(StreamBase stream = null, StreamBase oldStream = null, string error = null) { Stream = stream; OldStream= oldStream; Error = error; }
    }
    public class OpenAudioStreamCompletedArgs : StreamOpenedArgs
    {
        public new AudioStream Stream   => (AudioStream)base.Stream;
        public new AudioStream OldStream=> (AudioStream)base.OldStream;
        public OpenAudioStreamCompletedArgs(AudioStream stream = null, AudioStream oldStream = null, string error = null): base(stream, oldStream, error) { }
    }
    public class OpenVideoStreamCompletedArgs : StreamOpenedArgs
    {
        public new VideoStream Stream   => (VideoStream)base.Stream;
        public new VideoStream OldStream=> (VideoStream)base.OldStream;
        public OpenVideoStreamCompletedArgs(VideoStream stream = null, VideoStream oldStream = null, string error = null): base(stream, oldStream, error) { }
    }
    public class OpenSubtitlesStreamCompletedArgs : StreamOpenedArgs
    {
        public new SubtitlesStream Stream   => (SubtitlesStream)base.Stream;
        public new SubtitlesStream OldStream=> (SubtitlesStream)base.OldStream;
        public OpenSubtitlesStreamCompletedArgs(SubtitlesStream stream = null, SubtitlesStream oldStream = null, string error = null): base(stream, oldStream, error) { }
    }
    public class OpenDataStreamCompletedArgs : StreamOpenedArgs
    {
        public new DataStream Stream => (DataStream)base.Stream;
        public new DataStream OldStream => (DataStream)base.OldStream;
        public OpenDataStreamCompletedArgs(DataStream stream = null, DataStream oldStream = null, string error = null) : base(stream, oldStream, error) { }
    }
    public class ExternalStreamOpenedArgs : EventArgs
    {
        public ExternalStream   ExtStream;
        public ExternalStream   OldExtStream;
        public string           Error;
        public bool             Success => Error == null;
        public ExternalStreamOpenedArgs(ExternalStream extStream = null, ExternalStream oldExtStream = null, string error = null) { ExtStream = extStream; OldExtStream= oldExtStream; Error = error; }
    }
    public class OpenExternalAudioStreamCompletedArgs : ExternalStreamOpenedArgs
    {
        public new ExternalAudioStream ExtStream   => (ExternalAudioStream)base.ExtStream;
        public new ExternalAudioStream OldExtStream=> (ExternalAudioStream)base.OldExtStream;
        public OpenExternalAudioStreamCompletedArgs(ExternalAudioStream extStream = null, ExternalAudioStream oldExtStream = null, string error = null) : base(extStream, oldExtStream, error) { }
    }
    public class OpenExternalVideoStreamCompletedArgs : ExternalStreamOpenedArgs
    {
        public new ExternalVideoStream ExtStream   => (ExternalVideoStream)base.ExtStream;
        public new ExternalVideoStream OldExtStream=> (ExternalVideoStream)base.OldExtStream;
        public OpenExternalVideoStreamCompletedArgs(ExternalVideoStream extStream = null, ExternalVideoStream oldExtStream = null, string error = null) : base(extStream, oldExtStream, error) { }
    }
    public class OpenExternalSubtitlesStreamCompletedArgs : ExternalStreamOpenedArgs
    {
        public new ExternalSubtitlesStream ExtStream   => (ExternalSubtitlesStream)base.ExtStream;
        public new ExternalSubtitlesStream OldExtStream=> (ExternalSubtitlesStream)base.OldExtStream;
        public OpenExternalSubtitlesStreamCompletedArgs(ExternalSubtitlesStream extStream = null, ExternalSubtitlesStream oldExtStream = null, string error = null) : base(extStream, oldExtStream, error) { }
    }

    private void OnOpenCompleted(OpenCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        VideoDecoder.Renderer?.ClearScreen();
        if (CanInfo) Log.Info($"[Open] {args.Url ?? "None"} {(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenCompleted?.Invoke(this, args);
    }
    private void OnOpenSessionCompleted(OpenSessionCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        VideoDecoder.Renderer?.ClearScreen();
        if (CanInfo) Log.Info($"[OpenSession] {args.Session.Url ?? "None"} - Item: {args.Session.PlaylistItem} {(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenSessionCompleted?.Invoke(this, args);
    }
    private void OnOpenSubtitles(OpenSubtitlesCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        if (CanInfo) Log.Info($"[OpenSubtitles] {args.Url ?? "None"} {(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenSubtitlesCompleted?.Invoke(this, args);
    }
    private void OnOpenPlaylistItemCompleted(OpenPlaylistItemCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        VideoDecoder.Renderer?.ClearScreen();
        if (CanInfo) Log.Info($"[OpenPlaylistItem] {(args.OldItem != null ? args.OldItem.Title : "None")} => {(args.Item != null ? args.Item.Title : "None")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenPlaylistItemCompleted?.Invoke(this, args);
    }
    private void OnOpenAudioStreamCompleted(OpenAudioStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        ClosedAudioStream = null;
        MainDemuxer = !VideoDemuxer.Disposed ? VideoDemuxer : AudioDemuxer;

        if (CanInfo) Log.Info($"[OpenAudioStream] #{(args.OldStream != null ? args.OldStream.StreamIndex.ToString() : "_")} => #{(args.Stream != null ? args.Stream.StreamIndex.ToString() : "_")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenAudioStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenVideoStreamCompleted(OpenVideoStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        ClosedVideoStream = null;
        MainDemuxer = !VideoDemuxer.Disposed ? VideoDemuxer : AudioDemuxer;

        if (CanInfo) Log.Info($"[OpenVideoStream] #{(args.OldStream != null ? args.OldStream.StreamIndex.ToString() : "_")} => #{(args.Stream != null ? args.Stream.StreamIndex.ToString() : "_")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenVideoStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenSubtitlesStreamCompleted(OpenSubtitlesStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        if (CanInfo) Log.Info($"[OpenSubtitlesStream] #{(args.OldStream != null ? args.OldStream.StreamIndex.ToString() : "_")} => #{(args.Stream != null ? args.Stream.StreamIndex.ToString() : "_")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenSubtitlesStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenDataStreamCompleted(OpenDataStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        if (CanInfo)
            Log.Info($"[OpenDataStream] #{(args.OldStream != null ? args.OldStream.StreamIndex.ToString() : "_")} => #{(args.Stream != null ? args.Stream.StreamIndex.ToString() : "_")}{(!args.Success ? " [Error: " + args.Error + "]" : "")}");
        OpenDataStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenExternalAudioStreamCompleted(OpenExternalAudioStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        ClosedAudioStream = null;
        MainDemuxer = !VideoDemuxer.Disposed ? VideoDemuxer : AudioDemuxer;

        if (CanInfo) Log.Info($"[OpenExternalAudioStream] {(args.OldExtStream != null ? args.OldExtStream.Url : "None")} => {(args.ExtStream != null ? args.ExtStream.Url : "None")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenExternalAudioStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenExternalVideoStreamCompleted(OpenExternalVideoStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        ClosedVideoStream = null;
        MainDemuxer = !VideoDemuxer.Disposed ? VideoDemuxer : AudioDemuxer;

        if (CanInfo) Log.Info($"[OpenExternalVideoStream] {(args.OldExtStream != null ? args.OldExtStream.Url : "None")} => {(args.ExtStream != null ? args.ExtStream.Url : "None")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenExternalVideoStreamCompleted?.Invoke(this, args);
    }
    private void OnOpenExternalSubtitlesStreamCompleted(OpenExternalSubtitlesStreamCompletedArgs args = null)
    {
        if (shouldDispose)
        {
            Dispose();
            return;
        }

        if (CanInfo) Log.Info($"[OpenExternalSubtitlesStream] {(args.OldExtStream != null ? args.OldExtStream.Url : "None")} => {(args.ExtStream != null ? args.ExtStream.Url : "None")}{(!args.Success ? " [Error: " + args.Error  + "]": "")}");
        OpenExternalSubtitlesStreamCompleted?.Invoke(this, args);
    }
    #endregion

    #region Open
    public OpenCompletedArgs Open(string url, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        => Open((object)url, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);
    public OpenCompletedArgs Open(Stream iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        => Open((object)iostream, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);

    internal OpenCompletedArgs Open(object input, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        OpenCompletedArgs args = new();

        try
        {
            Initialize();

            if (input is Stream)
            {
                Playlist.IOStream = (Stream)input;
            }
            else
                Playlist.Url = input.ToString(); // TBR: check UI update

            args.Url = Playlist.Url;
            args.IOStream = Playlist.IOStream;
            args.Error = Open().Error;

            if (Playlist.Items.Count == 0 && args.Success)
                args.Error = "No playlist items were found";

            if (!args.Success)
                return args;

            if (!defaultPlaylistItem)
                return args;

            args.Error = Open(SuggestItem(), defaultVideo, defaultAudio, defaultSubtitles).Error;

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        }
        finally
        {
            OnOpenCompleted(args);
        }
    }
    public new OpenSubtitlesCompletedArgs OpenSubtitles(string url)
    {
        OpenSubtitlesCompletedArgs args = new();

        try
        {
            var res = base.OpenSubtitles(url);
            args.Error = res == null ? "No external subtitles stream found" : res.Error;

            if (args.Success)
                args.Error = Open(res.ExternalSubtitlesStream).Error;

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        }
        finally
        {
            OnOpenSubtitles(args);
        }
    }
    public OpenSessionCompletedArgs Open(Session session)
    {
        OpenSessionCompletedArgs args = new(session);

        try
        {
            // Open
            if (session.Url != null && session.Url != Playlist.Url) // && session.Url != Playlist.DirectUrl)
            {
                args.Error = Open(session.Url, false, false, false, false).Error;
                if (!args.Success)
                    return args;
            }

            // Open Item
            if (session.PlaylistItem != -1)
            {
                args.Error = Open(Playlist.Items[session.PlaylistItem], false, false, false).Error;
                if (!args.Success)
                    return args;
            }

            // Open Streams
            if (session.ExternalVideoStream != -1)
            {
                args.Error = Open(Playlist.Selected.ExternalVideoStreams[session.ExternalVideoStream], false, session.VideoStream).Error;
                if (!args.Success)
                    return args;
            }
            else if (session.VideoStream != -1)
            {
                args.Error = Open(VideoDemuxer.AVStreamToStream[session.VideoStream], false).Error;
                if (!args.Success)
                    return args;
            }

            string tmpErr = null;
            if (session.ExternalAudioStream != -1)
                tmpErr = Open(Playlist.Selected.ExternalAudioStreams[session.ExternalAudioStream], false, session.AudioStream).Error;
            else if (session.AudioStream != -1)
                tmpErr = Open(VideoDemuxer.AVStreamToStream[session.AudioStream], false).Error;

            if (tmpErr != null & VideoStream == null)
            {
                args.Error = tmpErr;
                return args;
            }

            if (session.ExternalSubtitlesUrl != null)
                OpenSubtitles(session.ExternalSubtitlesUrl);
            else if (session.SubtitlesStream != -1)
                Open(VideoDemuxer.AVStreamToStream[session.SubtitlesStream]);

            Config.Audio.SetDelay(session.AudioDelay);

            for (int i = 0; i < subNum; i++)
            {
                Config.Subtitles[i].SetDelay(session.SubtitlesDelay);
            }

            if (session.CurTime > 1 * (long)1000 * 10000)
                Seek(session.CurTime / 10000);

            return args;
        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenSessionCompleted(args);
        }
    }
    public OpenPlaylistItemCompletedArgs Open(PlaylistItem item, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        OpenPlaylistItemCompletedArgs args = new(item);

        try
        {
            long stoppedTime = GetCurTime();
            InitializeSwitch();

            // Disables old item
            if (Playlist.Selected != null)
            {
                args.OldItem = Playlist.Selected;
                Playlist.Selected.Enabled = false;
            }

            if (item == null)
            {
                args.Error = "Cancelled";
                return args;
            }

            Playlist.Selected = item;
            Playlist.Selected.Enabled = true;

            // We reset external streams of the current item and not the old one
            if (Playlist.Selected.ExternalAudioStream != null)
            {
                Playlist.Selected.ExternalAudioStream.Enabled = false;
                Playlist.Selected.ExternalAudioStream = null;
            }

            if (Playlist.Selected.ExternalVideoStream != null)
            {
                Playlist.Selected.ExternalVideoStream.Enabled = false;
                Playlist.Selected.ExternalVideoStream = null;
            }

            for (int i = 0; i < subNum; i++)
            {
                if (Playlist.Selected.ExternalSubtitlesStreams[i] != null)
                {
                    Playlist.Selected.ExternalSubtitlesStreams[i].Enabled = false;
                    Playlist.Selected.ExternalSubtitlesStreams[i] = null;
                }
            }

            args.Error = OpenItem().Error;

            if (!args.Success)
                return args;

            if (Playlist.Selected.Url != null || Playlist.Selected.IOStream != null)
                args.Error = OpenDemuxerInput(VideoDemuxer, Playlist.Selected);

            if (!args.Success)
                return args;

            if (defaultVideo && Config.Video.Enabled)
                args.Error = OpenSuggestedVideo(defaultAudio);
            else if (defaultAudio && Config.Audio.Enabled)
                args.Error = OpenSuggestedAudio();

            if ((defaultVideo || defaultAudio) && AudioStream == null && VideoStream == null)
            {
                args.Error ??= "No audio/video found";

                return args;
            }

            if (defaultSubtitles && Config.Subtitles.Enabled)
            {
                if (Playlist.Selected.ExternalSubtitlesStreams[0] != null)
                    Open(Playlist.Selected.ExternalSubtitlesStreams[0]);
                else
                    OpenSuggestedSubtitles();
            }

            if (Config.Data.Enabled)
            {
                OpenSuggestedData();
            }

            LoadPlaylistChapters();

            return args;
        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenPlaylistItemCompleted(args);
        }
    }
    public ExternalStreamOpenedArgs Open(ExternalStream extStream, bool defaultAudio = false, int streamIndex = -1) // -2: None, -1: Suggest, >=0: specified
    {
        ExternalStreamOpenedArgs args = null;

        try
        {
            Demuxer demuxer;

            if (extStream is ExternalVideoStream)
            {
                args = new OpenExternalVideoStreamCompletedArgs((ExternalVideoStream) extStream, Playlist.Selected.ExternalVideoStream);

                if (args.OldExtStream != null)
                    args.OldExtStream.Enabled = false;

                Playlist.Selected.ExternalVideoStream = (ExternalVideoStream) extStream;

                foreach(var plugin in Plugins.Values)
                    plugin.OnOpenExternalVideo();

                demuxer = VideoDemuxer;
            }
            else if (extStream is ExternalAudioStream)
            {
                args = new OpenExternalAudioStreamCompletedArgs((ExternalAudioStream) extStream, Playlist.Selected.ExternalAudioStream);

                if (args.OldExtStream != null)
                    args.OldExtStream.Enabled = false;

                Playlist.Selected.ExternalAudioStream = (ExternalAudioStream) extStream;

                foreach(var plugin in Plugins.Values)
                    plugin.OnOpenExternalAudio();

                demuxer = AudioDemuxer;
            }
            else
            {
                int i = SubtitlesSelectedHelper.CurIndex;
                args = new OpenExternalSubtitlesStreamCompletedArgs((ExternalSubtitlesStream) extStream, Playlist.Selected.ExternalSubtitlesStreams[i]);

                if (args.OldExtStream != null)
                    args.OldExtStream.Enabled = false;

                Playlist.Selected.ExternalSubtitlesStreams[i] = (ExternalSubtitlesStream) extStream;

                if (!Playlist.Selected.ExternalSubtitlesStreams[i].Downloaded)
                    DownloadSubtitles(Playlist.Selected.ExternalSubtitlesStreams[i]);

                foreach(var plugin in Plugins.Values)
                    plugin.OnOpenExternalSubtitles();

                demuxer = SubtitlesDemuxers[i];
            }

            // Open external stream
            args.Error = OpenDemuxerInput(demuxer, extStream);

            if (!args.Success)
                return args;

            // Update embedded streams with the external stream pointer
            foreach (var embStream in demuxer.VideoStreams)
                embStream.ExternalStream = extStream;
            foreach (var embStream in demuxer.AudioStreams)
                embStream.ExternalStream = extStream;
            foreach (var embStream in demuxer.SubtitlesStreamsAll)
            {
                embStream.ExternalStream = extStream;
                embStream.ExternalStreamAdded(); // Copies VobSub's .idx file to extradata (based on external url .sub)
            }

            // Open embedded stream
            if (streamIndex != -2)
            {
                StreamBase suggestedStream = null;
                if (streamIndex != -1 && (streamIndex >= demuxer.AVStreamToStream.Count || streamIndex < 0 || demuxer.AVStreamToStream[streamIndex].Type != extStream.Type))
                {
                    args.Error = $"Invalid stream index {streamIndex}";
                    demuxer.Dispose();
                    return args;
                }

                if (demuxer.Type == MediaType.Video)
                    suggestedStream = streamIndex == -1 ? SuggestVideo(demuxer.VideoStreams) : demuxer.AVStreamToStream[streamIndex];
                else if (demuxer.Type == MediaType.Audio)
                    suggestedStream = streamIndex == -1 ? SuggestAudio(demuxer.AudioStreams) : demuxer.AVStreamToStream[streamIndex];
                else if (demuxer.Type == MediaType.Subs)
                {
                    System.Collections.Generic.List<Language> langs = Config.Subtitles.Languages.ToList();
                    langs.Add(Language.Unknown);
                    suggestedStream = streamIndex == -1 ? SuggestSubtitles(demuxer.SubtitlesStreamsAll, langs) : demuxer.AVStreamToStream[streamIndex];
                }
                else
                {
                    suggestedStream = demuxer.AVStreamToStream[streamIndex];
                }

                if (suggestedStream == null)
                {
                    demuxer.Dispose();
                    args.Error = "No embedded streams found";
                    return args;
                }

                args.Error = Open(suggestedStream, defaultAudio).Error;
                if (!args.Success)
                    return args;
            }

            LoadPlaylistChapters();

            extStream.Enabled = true;

            return args;
        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            if (extStream is ExternalVideoStream)
                OnOpenExternalVideoStreamCompleted((OpenExternalVideoStreamCompletedArgs)args);
            else if (extStream is ExternalAudioStream)
                OnOpenExternalAudioStreamCompleted((OpenExternalAudioStreamCompletedArgs)args);
            else
                OnOpenExternalSubtitlesStreamCompleted((OpenExternalSubtitlesStreamCompletedArgs)args);
        }
    }

    public StreamOpenedArgs OpenVideoStream(VideoStream stream, bool defaultAudio = true)
        => Open(stream, defaultAudio);
    public StreamOpenedArgs OpenAudioStream(AudioStream stream)
        => Open(stream);
    public StreamOpenedArgs OpenSubtitlesStream(SubtitlesStream stream)
        => Open(stream);
    public StreamOpenedArgs OpenDataStream(DataStream stream)
        => Open(stream);
    private StreamOpenedArgs Open(StreamBase stream, bool defaultAudio = false)
    {
        StreamOpenedArgs args = null;

        try
        {
            lock (stream.Demuxer.lockActions)
            lock (stream.Demuxer.lockFmtCtx)
            {
                var oldStream = stream.Type == MediaType.Video ? VideoStream : (stream.Type == MediaType.Audio ? AudioStream : (StreamBase)DataStream);
                if (stream.Type == MediaType.Subs)
                {
                    oldStream = SubtitlesStreams[SubtitlesSelectedHelper.CurIndex];
                }

                // Close external demuxers when opening embedded
                if (stream.Demuxer.Type == MediaType.Video)
                {
                    // TBR: if (stream.Type == MediaType.Video) | We consider that we can't have Embedded and External Video Streams at the same time
                    if (stream.Type == MediaType.Audio) // TBR: && VideoStream != null)
                    {
                        if (!EnableDecoding) AudioDemuxer.Dispose();
                        if (Playlist.Selected.ExternalAudioStream != null)
                        {
                            Playlist.Selected.ExternalAudioStream.Enabled = false;
                            Playlist.Selected.ExternalAudioStream = null;
                        }
                    }
                    else if (stream.Type == MediaType.Subs)
                    {
                        int i = SubtitlesSelectedHelper.CurIndex;
                        if (!EnableDecoding)
                           SubtitlesDemuxers[i].Dispose();

                        if (Playlist.Selected.ExternalSubtitlesStreams[i] != null)
                        {
                            Playlist.Selected.ExternalSubtitlesStreams[i].Enabled = false;
                            Playlist.Selected.ExternalSubtitlesStreams[i] = null;
                        }
                    }
                    else if (stream.Type == MediaType.Data)
                    {
                        if (!EnableDecoding) DataDemuxer.Dispose();
                    }
                }
                else if (!EnableDecoding)
                {
                    // Disable embeded audio when enabling external audio (TBR)
                    if (stream.Demuxer.Type == MediaType.Audio && stream.Type == MediaType.Audio && AudioStream != null && AudioStream.Demuxer.Type == MediaType.Video)
                    {
                        foreach (var aStream in VideoDemuxer.AudioStreams)
                            VideoDemuxer.DisableStream(aStream);
                    }
                }

                // Open Codec / Enable on demuxer
                if (EnableDecoding)
                {
                    string ret = GetDecoderPtr(stream).Open(stream);

                    if (ret != null)
                    {
                        return stream.Type == MediaType.Video
                        ? (args = new OpenVideoStreamCompletedArgs((VideoStream)stream, (VideoStream)oldStream, $"Failed to open video stream #{stream.StreamIndex}\r\n{ret}"))
                        : stream.Type == MediaType.Audio
                        ? (args = new OpenAudioStreamCompletedArgs((AudioStream)stream, (AudioStream)oldStream, $"Failed to open audio stream #{stream.StreamIndex}\r\n{ret}"))
                        : stream.Type == MediaType.Subs
                        ? (args = new OpenSubtitlesStreamCompletedArgs((SubtitlesStream)stream, (SubtitlesStream)oldStream, $"Failed to open subtitles stream #{stream.StreamIndex}\r\n{ret}"))
                        : (args = new OpenDataStreamCompletedArgs((DataStream)stream, (DataStream)oldStream, $"Failed to open data stream #{stream.StreamIndex}\r\n{ret}"));
                    }
                }
                else
                    stream.Demuxer.EnableStream(stream);

                // Open Audio based on new Video Stream (if not the same suggestion)
                if (defaultAudio && stream.Type == MediaType.Video && Config.Audio.Enabled)
                {
                    bool requiresChange = true;
                    SuggestAudio(out var aStream, out var aExtStream, VideoDemuxer.AudioStreams);

                    if (AudioStream != null)
                    {
                        // External audio streams comparison
                        if (Playlist.Selected.ExternalAudioStream != null && aExtStream != null && aExtStream.Index == Playlist.Selected.ExternalAudioStream.Index)
                            requiresChange = false;
                        // Embedded audio streams comparison
                        else if (Playlist.Selected.ExternalAudioStream == null && aStream != null && aStream.StreamIndex == AudioStream.StreamIndex)
                            requiresChange = false;
                    }

                    if (!requiresChange)
                    {
                        if (CanDebug) Log.Debug($"Audio no need to follow video");
                    }
                    else
                    {
                         if (aStream != null)
                            Open(aStream);
                        else if (aExtStream != null)
                            Open(aExtStream);

                         //RequiresResync = true;
                    }
                }

                    return stream.Type == MediaType.Video
                    ? (args = new OpenVideoStreamCompletedArgs((VideoStream)stream, (VideoStream)oldStream))
                    : stream.Type == MediaType.Audio
                    ? (args = new OpenAudioStreamCompletedArgs((AudioStream)stream, (AudioStream)oldStream))
                    : stream.Type == MediaType.Subs
                    ? (args = new OpenSubtitlesStreamCompletedArgs((SubtitlesStream)stream, (SubtitlesStream)oldStream))
                    : (args = new OpenDataStreamCompletedArgs((DataStream)stream, (DataStream)oldStream));
                }
        } catch(Exception e)
        {
            return args = new StreamOpenedArgs(null, null, e.Message);
        } finally
        {
            if (stream.Type == MediaType.Video)
                OnOpenVideoStreamCompleted((OpenVideoStreamCompletedArgs)args);
            else if (stream.Type == MediaType.Audio)
                OnOpenAudioStreamCompleted((OpenAudioStreamCompletedArgs)args);
            else if (stream.Type == MediaType.Subs)
                OnOpenSubtitlesStreamCompleted((OpenSubtitlesStreamCompletedArgs)args);
            else
                OnOpenDataStreamCompleted((OpenDataStreamCompletedArgs)args);
        }
    }

    public string OpenSuggestedVideo(bool defaultAudio = false)
    {
        VideoStream stream;
        ExternalVideoStream extStream;
        string error = null;

        if (ClosedVideoStream != null)
        {
            Log.Debug("[Video] Found previously closed stream");

            extStream = ClosedVideoStream.Item1;
            if (extStream != null)
                return Open(extStream, false, ClosedVideoStream.Item2 >= 0 ? ClosedVideoStream.Item2 : -1).Error;

            stream = ClosedVideoStream.Item2 >= 0 ? (VideoStream)VideoDemuxer.AVStreamToStream[ClosedVideoStream.Item2] : null;
        }
        else
            SuggestVideo(out stream, out extStream, VideoDemuxer.VideoStreams);

        if (stream != null)
            error = Open(stream, defaultAudio).Error;
        else if (extStream != null)
            error = Open(extStream, defaultAudio).Error;
        else if (defaultAudio && Config.Audio.Enabled)
            error = OpenSuggestedAudio(); // We still need audio if no video exists

        return error;
    }
    public string OpenSuggestedAudio()
    {
        AudioStream stream = null;
        ExternalAudioStream extStream = null;
        string error = null;

        if (ClosedAudioStream != null)
        {
            Log.Debug("[Audio] Found previously closed stream");

            extStream = ClosedAudioStream.Item1;
            if (extStream != null)
                return Open(extStream, false, ClosedAudioStream.Item2 >= 0 ? ClosedAudioStream.Item2 : -1).Error;

            stream = ClosedAudioStream.Item2 >= 0 ? (AudioStream)VideoDemuxer.AVStreamToStream[ClosedAudioStream.Item2] : null;
        }
        else
            SuggestAudio(out stream, out extStream, VideoDemuxer.AudioStreams);

        if (stream != null)
            error = Open(stream).Error;
        else if (extStream != null)
            error = Open(extStream).Error;

        return error;
    }
    public void OpenSuggestedSubtitles(int? subIndex = -1)
    {
        long sessionId = OpenItemCounter;

        try
        {
            // High Suggest (first lang priority + high rating + already converted/downloaded)
            // 1. Check embedded steams for high suggest
            if (Config.Subtitles.Languages.Count > 0)
            {
                foreach (var stream in VideoDemuxer.SubtitlesStreamsAll)
                {
                    if (stream.Language == Config.Subtitles.Languages[0])
                    {
                        Log.Debug("[Subtitles] Found high suggested embedded stream");
                        Open(stream);
                        return;
                    }
                }
            }

            // 2. Check external streams for high suggest
            if (Playlist.Selected.ExternalSubtitlesStreamsAll.Count > 0)
            {
                var extStream = SuggestBestExternalSubtitles();
                if (extStream != null)
                {
                    Log.Debug("[Subtitles] Found high suggested external stream");
                    Open(extStream);
                    return;
                }
            }

            // 3. Search offline if allowed
            if (SearchLocalSubtitles())
            {
                // 3.1 Check external streams for high suggest (again for the new additions if any)
                ExternalSubtitlesStream extStream = SuggestBestExternalSubtitles();
                if (extStream != null)
                {
                    Log.Debug("[Subtitles] Found high suggested local external stream");
                    Open(extStream);
                    return;
                }
            }

        } catch (Exception e)
        {
            Log.Debug($"OpenSuggestedSubtitles canceled? [{e.Message}]");
            return;
        }

        Task.Run(() =>
        {
            try
            {
                if (sessionId != OpenItemCounter)
                {
                    Log.Debug("OpenSuggestedSubtitles canceled");
                    return;
                }

                if (sessionId != OpenItemCounter)
                {
                    Log.Debug("OpenSuggestedSubtitles canceled");
                    return;
                }

                // 4. Search online if allowed (not async)
                SearchOnlineSubtitles();

                if (sessionId != OpenItemCounter)
                {
                    Log.Debug("OpenSuggestedSubtitles canceled");
                    return;
                }

                // 5. (Any) Check embedded/external streams for config languages (including 'undefined')
                SuggestSubtitles(out var stream, out ExternalSubtitlesStream extStream);

                if (stream != null)
                    Open(stream);
                else if (extStream != null)
                    Open(extStream);
            } catch (Exception e)
            {
                Log.Debug($"OpenSuggestedSubtitles canceled? [{e.Message}]");
            }
        });
    }
    public string OpenSuggestedData()
    {
        DataStream stream;
        string error = null;

        SuggestData(out stream, VideoDemuxer.DataStreams);

        if (stream != null)
            error = Open(stream).Error;

        return error;
    }

    public string OpenDemuxerInput(Demuxer demuxer, DemuxerInput demuxerInput)
    {
        OpenedPlugin?.OnBuffering();

        string error = null;

        Dictionary<string, string> formatOpt = null;
        Dictionary<string, string> copied = null;

        try
        {
            // Set HTTP Config
            if (Playlist.InputType == InputType.Web)
            {
                formatOpt = Config.Demuxer.GetFormatOptPtr(demuxer.Type);
                copied = new Dictionary<string, string>();

                foreach (var opt in formatOpt)
                    copied.Add(opt.Key, opt.Value);

                if (demuxerInput.UserAgent != null)
                    formatOpt["user_agent"] = demuxerInput.UserAgent;

                if (demuxerInput.Referrer != null)
                    formatOpt["referer"] = demuxerInput.Referrer;

                // this can cause issues
                //else if (!formatOpt.ContainsKey("referer") && Playlist.Url != null)
                //    formatOpt["referer"] = Playlist.Url;

                if (demuxerInput.HTTPHeaders != null)
                {
                    formatOpt["headers"] = "";
                    foreach(var header in demuxerInput.HTTPHeaders)
                        formatOpt["headers"] += header.Key + ": " + header.Value + "\r\n";
                }
            }

            // Open Demuxer Input
            if (demuxerInput.Url != null)
            {
                error = demuxer.Open(demuxerInput.Url);

                if (error != null && !string.IsNullOrEmpty(demuxerInput.UrlFallback))
                {
                    Log.Warn($"Fallback to {demuxerInput.UrlFallback}");
                    error = demuxer.Open(demuxerInput.UrlFallback);
                }
            }
            else if (demuxerInput.IOStream != null)
                error = demuxer.Open(demuxerInput.IOStream);

            return error;
        } finally
        {
            // Restore HTTP Config
            if (Playlist.InputType == InputType.Web)
            {
                formatOpt.Clear();
                foreach(var opt in copied)
                    formatOpt.Add(opt.Key, opt.Value);
            }

            OpenedPlugin?.OnBufferingCompleted();
        }
    }

    private void LoadPlaylistChapters()
    {
        if (Playlist.Selected != null && Playlist.Selected.Chapters.Count > 0 && MainDemuxer.Chapters.Count == 0)
        {
            foreach (var chapter in Playlist.Selected.Chapters)
            {
                MainDemuxer.Chapters.Add(chapter);
            }
        }
    }
    #endregion

    #region Close (Only For EnableDecoding)
    public void CloseAudio()
    {
        ClosedAudioStream = new Tuple<ExternalAudioStream, int>(Playlist.Selected.ExternalAudioStream, AudioStream != null ? AudioStream.StreamIndex : -1);

        if (Playlist.Selected.ExternalAudioStream != null)
        {
            Playlist.Selected.ExternalAudioStream.Enabled = false;
            Playlist.Selected.ExternalAudioStream = null;
        }

        AudioDecoder.Dispose(true);
    }
    public void CloseVideo()
    {
        ClosedVideoStream = new Tuple<ExternalVideoStream, int>(Playlist.Selected.ExternalVideoStream, VideoStream != null ? VideoStream.StreamIndex : -1);

        if (Playlist.Selected.ExternalVideoStream != null)
        {
            Playlist.Selected.ExternalVideoStream.Enabled = false;
            Playlist.Selected.ExternalVideoStream = null;
        }

        VideoDecoder.Dispose(true);
        VideoDecoder.Renderer?.ClearScreen();
    }
    public void CloseSubtitles(int subIndex)
    {
        if (Playlist.Selected.ExternalSubtitlesStreams[subIndex] != null)
        {
            Playlist.Selected.ExternalSubtitlesStreams[subIndex].Enabled = false;
            Playlist.Selected.ExternalSubtitlesStreams[subIndex] = null;
        }

        SubtitlesDecoders[subIndex].Dispose(true);
    }
    public void CloseData()
    {
        DataDecoder.Dispose(true);
    }
    #endregion
}

using System.Windows;
using System.Windows.Controls;
using FlyleafLib;
using LLPlayer.Extensions;
using LLPlayer.Services;
using LLPlayer.Views;

namespace LLPlayer.Controls.Settings;

public partial class SettingsAudio : UserControl
{
    public SettingsAudio()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsAudioVM>();
    }
}

public class SettingsAudioVM : Bindable
{
    private readonly IDialogService _dialogService;
    public FlyleafManager FL { get; }

    public SettingsAudioVM(FlyleafManager fl, IDialogService dialogService)
    {
        _dialogService = dialogService;
        FL = fl;
    }

    public DelegateCommand? CmdConfigureLanguage => field ??= new(() =>
    {
        DialogParameters p = new()
        {
            { "languages", FL.PlayerConfig.Audio.Languages }
        };

        _dialogService.ShowDialog(nameof(SelectLanguageDialog), p, result =>
        {
            List<Language> updated = result.Parameters.GetValue<List<Language>>("languages");

            if (!FL.PlayerConfig.Audio.Languages.SequenceEqual(updated))
            {
                FL.PlayerConfig.Audio.Languages = updated;
            }
        });
    });
}

using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FlyleafLib.MediaPlayer.Translation.Services;
using static FlyleafLib.Logger;

namespace FlyleafLib.MediaPlayer.Translation;

#nullable enable

public class SubTranslator
{
    private readonly SubManager _subManager;
    private readonly Config.SubtitlesConfig _config;
    private readonly int _subIndex;

    private ITranslateService? _translateService;
    private CancellationTokenSource? _translationCancellation;
    private readonly TranslateServiceFactory _translateServiceFactory;
    private Task? _translateTask;
    private bool _isReset;
    private Language? _srcLang;

    private bool IsEnabled => _config[_subIndex].EnabledTranslated;

    private readonly LogHandler Log;

    public SubTranslator(SubManager subManager, Config.SubtitlesConfig config, int subIndex)
    {
        _subManager = subManager;
        _config = config;
        _subIndex = subIndex;

        _subManager.PropertyChanged += SubManager_OnPropertyChanged;

        // apply config changes
        _config.PropertyChanged += SubtitlesConfig_OnPropertyChanged;
        _config.SubConfigs[subIndex].PropertyChanged += SubConfig_OnPropertyChanged;
        _config.TranslateChatConfig.PropertyChanged += TranslateChatConfig_OnPropertyChanged;

        _translateServiceFactory = new TranslateServiceFactory(config);

        Log = new LogHandler(("[#1]").PadRight(8, ' ') + $" [Translator{subIndex + 1}   ] ");
    }

    private int _oldIndex = -1;
    private CancellationTokenSource? _translationStartCancellation;

    private void SubManager_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(SubManager.CurrentIndex):
                if (!IsEnabled || _subManager.Subs.Count == 0 || _subManager.Language == null)
                    return;

                if (_translateService == null)
                {
                    // capture for later initialization
                    _srcLang = _subManager.Language;
                }

                if (_translationStartCancellation != null)
                {
                    _translationStartCancellation.Cancel();
                    _translationStartCancellation.Dispose();
                    _translationStartCancellation = null;
                }
                _translationStartCancellation = new CancellationTokenSource();
                _ = UpdateCurrentIndexAsync(_subManager.CurrentIndex, _translationStartCancellation.Token);
                break;
            case nameof(SubManager.Language):
                _ = Reset();
                break;
        }
    }

    private void SubtitlesConfig_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        string languageFallbackPropName = _subIndex == 0 ?
            nameof(Config.SubtitlesConfig.LanguageFallbackPrimary) :
            nameof(Config.SubtitlesConfig.LanguageFallbackSecondary);

        if (e.PropertyName is
            nameof(Config.SubtitlesConfig.TranslateServiceType) or
            nameof(Config.SubtitlesConfig.TranslateTargetLanguage)
            ||
            e.PropertyName == languageFallbackPropName)
        {
            // Apply translating config changes
            _ = Reset();
        }
    }

    private void SubConfig_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        // Reset when translation is off
        if (e.PropertyName is nameof(Config.SubConfig.EnabledTranslated))
        {
            if (!IsEnabled)
            {
                _ = Reset();
            }
        }
    }

    private void TranslateChatConfig_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        _ = Reset();
    }

    private async Task Reset()
    {
        if (_translateService == null)
            return;

        try
        {
            _isReset = true;
            await Cancel();

            _translateService?.Dispose();
            _translateService = null;
            _srcLang = null;
        }
        finally
        {
            _isReset = false;
        }
    }

    private async Task Cancel()
    {
        _translationCancellation?.Cancel();
        Task? pendingTask = _translateTask;
        if (pendingTask != null)
        {
            await pendingTask;
        }
        _translateTask = null;
    }

    private async Task UpdateCurrentIndexAsync(int newIndex, CancellationToken token)
    {
        if (newIndex != -1 && _subManager.Subs.Any())
        {
            int indexDiff = Math.Abs(_oldIndex - newIndex);
            bool isForward = newIndex > _oldIndex;
            _oldIndex = newIndex;

            if ((isForward && indexDiff > _config.TranslateCountForward) ||
                (!isForward && indexDiff > _config.TranslateCountBackward))
            {
                // Cancel a running translation request when a large seek is performed
                await Cancel();
            }
            else if (_translateTask != null)
            {
                // for performance
                return;
            }

            // Prevent continuous firing when continuously switching subtitles with sub seek
            if (indexDiff == 1)
            {
                if (_subManager.Subs[newIndex].Duration.TotalMilliseconds > 320)
                {
                    await Task.Delay(300, token);
                }
            }
            token.ThrowIfCancellationRequested();

            if (_translateTask == null && !_isReset)
            {
                // singleton task
                // Ensure that it is not executed in the main thread because it scans all subtitles
                Task task = Task.Run(async () =>
                {
                    try
                    {
                        await TranslateAheadAsync(newIndex, _config.TranslateCountBackward, _config.TranslateCountForward);
                    }
                    finally
                    {
                        _translateTask = null;
                    }
                });
                _translateTask = task;
            }
        }
    }

    private readonly Lock _initLock = new();
    // initialize TranslateService lazily
    private void EnsureTranslationService()
    {
        if (_translateService != null)
        {
            return;
        }

        // double-check lock pattern
        lock (_initLock)
        {
            if (_translateService == null)
            {
                var service = _translateServiceFactory.GetService(_config.TranslateServiceType, false);
                service.Initialize(_srcLang!, _config.TranslateTargetLanguage);

                Volatile.Write(ref _translateService, service);
            }
        }
    }

    private async Task TranslateAheadAsync(int currentIndex, int countBackward, int countForward)
    {
        try
        {
            // Token for canceling translation, releasing previous one to prevent leakage
            _translationCancellation?.Dispose();
            _translationCancellation = new CancellationTokenSource();

            var token = _translationCancellation.Token;
            int start = Math.Max(0, currentIndex - countBackward);
            int end = Math.Min(start + countForward - 1, _subManager.Subs.Count - 1);

            List<SubtitleData> translateSubs = new();
            for (int i = start; i <= end; i++)
            {
                if (token.IsCancellationRequested)
                    break;
                if (i >= _subManager.Subs.Count)
                    break;

                var sub = _subManager.Subs[i];
                if (!sub.IsTranslated && !string.IsNullOrEmpty(sub.Text))
                {
                    translateSubs.Add(sub);
                }
            }

            if (translateSubs.Count == 0)
                return;

            int concurrency = _config.TranslateMaxConcurrency;

            if (concurrency > 1 && _config.TranslateServiceType.IsLLM() &&
                _config.TranslateChatConfig.TranslateMethod == ChatTranslateMethod.KeepContext)
            {
                // fixed to 1
                // it must be sequential because of maintaining context
                concurrency = 1;
            }

            if (concurrency <= 1)
            {
                // sequentially (important to maintain execution order for LLM)
                foreach (var sub in translateSubs)
                {
                    await TranslateSubAsync(sub, token);
                }
            }
            else
            {
                // concurrently
                ParallelOptions parallelOptions = new()
                {
                    CancellationToken = token,
                    MaxDegreeOfParallelism = concurrency
                };

                await Parallel.ForEachAsync(
                    translateSubs,
                    parallelOptions,
                    async (sub, ct) =>
                    {
                        await TranslateSubAsync(sub, ct);
                    });
            }
        }
        catch (OperationCanceledException)
        {
            // ignore
        }
        catch (Exception ex)
        {
            Log.Error($"Translation failed: {ex.Message}");

            bool isConfigError = ex is TranslationConfigException;

            // Unable to translate, so turn off the translation and notify
            _config[_subIndex].EnabledTranslated = false;
            _ = Reset().ContinueWith((_) =>
            {
                if (isConfigError)
                {
                    _config.player.RaiseKnownErrorOccurred($"Translation Failed: {ex.Message}", KnownErrorType.Configuration);
                }
                else
                {
                    _config.player.RaiseUnknownErrorOccurred($"Translation Failed: {ex.Message}", UnknownErrorType.Translation, ex);
                }
            });
        }
    }

    private async Task TranslateSubAsync(SubtitleData sub, CancellationToken token)
    {
        string? text = sub.Text;
        if (string.IsNullOrEmpty(text))
            return;

        try
        {
            long start = Stopwatch.GetTimestamp();
            string translateText = SubtitleTextUtil.FlattenText(text);
            if (CanDebug) Log.Debug($"Translation Start {sub.Index} - {translateText}");
            EnsureTranslationService();
            string translated = await _translateService!.TranslateAsync(translateText, token);
            sub.TranslatedText = translated;

            if (CanDebug)
            {
                TimeSpan elapsed = Stopwatch.GetElapsedTime(start);
                Log.Debug($"Translation End {sub.Index} in {elapsed.TotalMilliseconds} - {translated}");
            }
        }
        catch (OperationCanceledException)
        {
            if (CanDebug) Log.Debug($"Translation Cancel {sub.Index}");
            throw;
        }
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class SubtitlesSidebar : UserControl
{
    private SubtitlesSidebarVM VM => (SubtitlesSidebarVM)DataContext;
    public SubtitlesSidebar()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SubtitlesSidebarVM>();

        Loaded += (sender, args) =>
        {
            VM.RequestScrollToTop += OnRequestScrollToTop;
        };

        Unloaded += (sender, args) =>
        {
            VM.RequestScrollToTop -= OnRequestScrollToTop;
            VM.Dispose();
        };
    }

    /// <summary>
    /// Scroll to the current subtitle
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void SubtitleListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (IsLoaded && SubtitleListBox.SelectedItem != null)
        {
            SubtitleListBox.ScrollIntoView(SubtitleListBox.SelectedItem);
        }
    }

    /// <summary>
    /// Scroll to the top subtitle
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private void OnRequestScrollToTop(object? sender, EventArgs args)
    {
        if (SubtitleListBox.Items.Count <= 0)
            return;

        var first = SubtitleListBox.Items[0];
        if (first != null)
        {
            SubtitleListBox.ScrollIntoView(first);
        }
    }

    private void SelectableTextBox_OnWordClicked(object? sender, WordClickedEventArgs e)
    {
        _ = WordPopupControl.OnWordClicked(e);
    }
}

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading.Tasks;

using FlyleafLib.MediaFramework.MediaDecoder;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

using static FlyleafLib.Logger;
using static FlyleafLib.MediaFramework.MediaContext.DecoderContext;
using static FlyleafLib.Utils;

namespace FlyleafLib.MediaPlayer;

unsafe partial class Player
{
    #region Events

    public event EventHandler<OpeningArgs>                              Opening; // Will be also used for subtitles
    public event EventHandler<OpenCompletedArgs>                        OpenCompleted; // Will be also used for subtitles
    public event EventHandler<OpenPlaylistItemCompletedArgs>            OpenPlaylistItemCompleted;
    public event EventHandler<OpenSessionCompletedArgs>                 OpenSessionCompleted;

    public event EventHandler<OpenAudioStreamCompletedArgs>             OpenAudioStreamCompleted;
    public event EventHandler<OpenVideoStreamCompletedArgs>             OpenVideoStreamCompleted;
    public event EventHandler<OpenSubtitlesStreamCompletedArgs>         OpenSubtitlesStreamCompleted;
    public event EventHandler<OpenDataStreamCompletedArgs>              OpenDataStreamCompleted;

    public event EventHandler<OpenExternalAudioStreamCompletedArgs>     OpenExternalAudioStreamCompleted;
    public event EventHandler<OpenExternalVideoStreamCompletedArgs>     OpenExternalVideoStreamCompleted;
    public event EventHandler<OpenExternalSubtitlesStreamCompletedArgs> OpenExternalSubtitlesStreamCompleted;

    private void OnOpening(OpeningArgs args = null)
        => Opening?.Invoke(this, args);
    private void OnOpenCompleted(OpenCompletedArgs args = null)
        => OpenCompleted?.Invoke(this, args);
    private void OnOpenSessionCompleted(OpenSessionCompletedArgs args = null)
        => OpenSessionCompleted?.Invoke(this, args);
    private void OnOpenPlaylistItemCompleted(OpenPlaylistItemCompletedArgs args = null)
        => OpenPlaylistItemCompleted?.Invoke(this, args);

    private void OnOpenAudioStreamCompleted(OpenAudioStreamCompletedArgs args = null)
        => OpenAudioStreamCompleted?.Invoke(this, args);
    private void OnOpenVideoStreamCompleted(OpenVideoStreamCompletedArgs args = null)
        => OpenVideoStreamCompleted?.Invoke(this, args);
    private void OnOpenSubtitlesStreamCompleted(OpenSubtitlesStreamCompletedArgs args = null)
        => OpenSubtitlesStreamCompleted?.Invoke(this, args);
    private void OnOpenDataStreamCompleted(OpenDataStreamCompletedArgs args = null)
        => OpenDataStreamCompleted?.Invoke(this, args);

    private void OnOpenExternalAudioStreamCompleted(OpenExternalAudioStreamCompletedArgs args = null)
        => OpenExternalAudioStreamCompleted?.Invoke(this, args);
    private void OnOpenExternalVideoStreamCompleted(OpenExternalVideoStreamCompletedArgs args = null)
        => OpenExternalVideoStreamCompleted?.Invoke(this, args);
    private void OnOpenExternalSubtitlesStreamCompleted(OpenExternalSubtitlesStreamCompletedArgs args = null)
        => OpenExternalSubtitlesStreamCompleted?.Invoke(this, args);
    #endregion

    #region Decoder Events
    private void Decoder_AudioCodecChanged(DecoderBase x)
    {
        Audio.Refresh();
        UIAll();
    }
    private void Decoder_VideoCodecChanged(DecoderBase x)
    {
        Video.Refresh();
        UIAll();
    }

    private void Decoder_OpenAudioStreamCompleted(object sender, OpenAudioStreamCompletedArgs e)
    {
        Config.Audio.SetDelay(0);
        Audio.Refresh();
        canPlay = Video.IsOpened || Audio.IsOpened;
        isLive  = MainDemuxer.IsLive;
        duration= MainDemuxer.Duration;

        UIAdd(() =>
        {
            IsLive  = IsLive;
            CanPlay = CanPlay;
            Duration=Duration;
        });
        UIAll();
    }
    private void Decoder_OpenVideoStreamCompleted(object sender, OpenVideoStreamCompletedArgs e)
    {
        Video.Refresh();
        canPlay = Video.IsOpened || Audio.IsOpened;
        isLive  = MainDemuxer.IsLive;
        duration= MainDemuxer.Duration;

        UIAdd(() =>
        {
            IsLive  = IsLive;
            CanPlay = CanPlay;
            Duration=Duration;
        });
        UIAll();
    }
    private void Decoder_OpenSubtitlesStreamCompleted(object sender, OpenSubtitlesStreamCompletedArgs e)
    {
        int i = SubtitlesSelectedHelper.CurIndex;

        Config.Subtitles[i].SetDelay(0);
        Subtitles[i].Refresh();
        UIAll();

        if (IsPlaying && Config.Subtitles.Enabled) // TBR (First run mainly with -from DecoderContext->OpenSuggestedSubtitles-> Task.Run causes late open, possible resync?)
        {
            if (!Subtitles[i].IsOpened)
            {
                return;
            }

            lock (lockSubtitles)
            {
                if (SubtitlesDecoders[i].OnVideoDemuxer)
                {
                    SubtitlesDecoders[i].Start();
                }
                //else// if (!decoder.RequiresResync)
                //{
                //    SubtitlesDemuxers[i].Start();
                //    SubtitlesDecoders[i].Start();
                //}
            }
        }
    }

    private void Decoder_OpenDataStreamCompleted(object sender, OpenDataStreamCompletedArgs e)
    {
        Data.Refresh();
        UIAll();
        if (Config.Data.Enabled)
        {
            lock (lockSubtitles)
                if (DataDecoder.OnVideoDemuxer)
                    DataDecoder.Start();
                else// if (!decoder.RequiresResync)
                {
                    DataDemuxer.Start();
                    DataDecoder.Start();
                }
        }
    }

    private void Decoder_OpenExternalAudioStreamCompleted(object sender, OpenExternalAudioStreamCompletedArgs e)
    {
        if (!e.Success)
        {
            canPlay = Video.IsOpened || Audio.IsOpened;
            UIAdd(() => CanPlay = CanPlay);
            UIAll();
        }
    }
    private void Decoder_OpenExternalVideoStreamCompleted(object sender, OpenExternalVideoStreamCompletedArgs e)
    {
        if (!e.Success)
        {
            canPlay = Video.IsOpened || Audio.IsOpened;
            UIAdd(() => CanPlay = CanPlay);
            UIAll();
        }
    }
    private void Decoder_OpenExternalSubtitlesStreamCompleted(object sender, OpenExternalSubtitlesStreamCompletedArgs e)
    {
        if (e.Success)
        {
            lock (lockSubtitles)
                ReSync(decoder.SubtitlesStreams[SubtitlesSelectedHelper.CurIndex], decoder.GetCurTimeMs());
        }
    }
    #endregion

    #region Open Implementation
    private OpenCompletedArgs OpenInternal(object url_iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        OpenCompletedArgs args = new();

        try
        {
            if (url_iostream == null)
            {
                args.Error = "Invalid empty/null input";
                return args;
            }

            if (CanInfo) Log.Info($"Opening {url_iostream}");

            Initialize(Status.Opening);
            var args2 = decoder.Open(url_iostream, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);

            args.Url = args2.Url;
            args.IOStream = args2.IOStream;
            args.Error = args2.Error;

            if (!args.Success)
            {
                status = Status.Failed;
                lastError = args.Error;
            }
            else if (CanPlay)
            {
                status = Status.Paused;

                if (Config.Player.AutoPlay)
                    Play();
            }
            else if (!defaultVideo && !defaultAudio)
            {
                isLive  = MainDemuxer.IsLive;
                duration= MainDemuxer.Duration;
                UIAdd(() =>
                {
                    IsLive  = IsLive;
                    Duration=Duration;
                });
            }

            UIAdd(() =>
            {
                LastError=LastError;
                Status  = Status;
            });

            UIAll();

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenCompleted(args);
        }
    }
    private OpenCompletedArgs OpenSubtitles(string url)
    {
        OpenCompletedArgs args = new(url, null, null, true);

        try
        {
            if (CanInfo) Log.Info($"Opening subtitles {url}");

            if (!Video.IsOpened)
            {
                args.Error = "Cannot open subtitles without video";
                return args;
            }

            Config.Subtitles.SetEnabled(true);
            args.Error = decoder.OpenSubtitles(url).Error;

            if (args.Success)
                ReSync(decoder.SubtitlesStreams[SubtitlesSelectedHelper.CurIndex]);

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenCompleted(args);
        }
    }

    /// <summary>
    /// Opens a new media file (audio/subtitles/video)
    /// </summary>
    /// <param name="url">Media file's url</param>
    /// <param name="defaultPlaylistItem">Whether to open the first/default item in case of playlist</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    /// <param name="forceSubtitles">Forces input url to be handled as subtitles</param>
    /// <returns></returns>
    public OpenCompletedArgs Open(string url, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true, bool forceSubtitles = false)
    {
        if (forceSubtitles || ExtensionsSubtitles.Contains(GetUrlExtention(url)))
        {
            OnOpening(new() { Url = url, IsSubtitles = true});
            return OpenSubtitles(url);
        }
        else
        {
            OnOpening(new() { Url = url });
            return OpenInternal(url, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);
        }
    }

    /// <summary>
    /// Opens a new media file (audio/subtitles/video) without blocking
    /// You can get the results from <see cref="OpenCompleted"/>
    /// </summary>
    /// <param name="url">Media file's url</param>
    /// <param name="defaultPlaylistItem">Whether to open the first/default item in case of playlist</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    public void OpenAsync(string url, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        => OpenAsyncPush(url, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);

    /// <summary>
    /// Opens a new media I/O stream (audio/video) without blocking
    /// </summary>
    /// <param name="iostream">Media stream</param>
    /// <param name="defaultPlaylistItem">Whether to open the first/default item in case of playlist</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    /// <returns></returns>
    public OpenCompletedArgs Open(Stream iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        OnOpening(new() { IOStream = iostream });
        return OpenInternal(iostream, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);
    }

    /// <summary>
    /// Opens a new media I/O stream (audio/video) without blocking
    /// You can get the results from <see cref="OpenCompleted"/>
    /// </summary>
    /// <param name="iostream">Media stream</param>
    /// <param name="defaultPlaylistItem">Whether to open the first/default item in case of playlist</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    public void OpenAsync(Stream iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        => OpenAsyncPush(iostream, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles);

    /// <summary>
    /// Opens a new media session
    /// </summary>
    /// <param name="session">Media session</param>
    /// <returns></returns>
    public OpenSessionCompletedArgs Open(Session session)
    {
        OpenSessionCompletedArgs args = new(session);

        try
        {
            Playlist.Selected?.AddTag(GetCurrentSession(), playerSessionTag);

            Initialize(Status.Opening, true, session.isReopen);
            args.Error = decoder.Open(session).Error;

            if (!args.Success || !CanPlay)
            {
                status = Status.Failed;
                lastError = args.Error;
            }
            else
            {
                status = Status.Paused;

                if (Config.Player.AutoPlay)
                    Play();
            }

            UIAdd(() =>
            {
                LastError=LastError;
                Status  = Status;
            });

            UIAll();

            return args;
        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenSessionCompleted(args);
        }
    }

    /// <summary>
    /// Opens a new media session without blocking
    /// </summary>
    /// <param name="session">Media session</param>
    public void OpenAsync(Session session) => OpenAsyncPush(session);

    /// <summary>
    /// Opens a playlist item <see cref="Playlist.Items"/>
    /// </summary>
    /// <param name="item">The playlist item to open</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    /// <returns></returns>
    public OpenPlaylistItemCompletedArgs Open(PlaylistItem item, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        OpenPlaylistItemCompletedArgs args = new(item, Playlist.Selected);

        try
        {
            Playlist.Selected?.AddTag(GetCurrentSession(), playerSessionTag);

            Initialize(Status.Opening, true, true);

            // TODO: Config.Player.Reopen? to reopen session if (item.OpenedCounter > 0)
            args = decoder.Open(item, defaultVideo, defaultAudio, defaultSubtitles);

            if (!args.Success)
            {
                status = Status.Failed;
                lastError = args.Error;
            }
            else if (CanPlay)
            {
                status = Status.Paused;

                if (Config.Player.AutoPlay)
                    Play();
                // TODO: else Show on frame?
            }
            else if (!defaultVideo && !defaultAudio)
            {
                isLive  = MainDemuxer.IsLive;
                duration= MainDemuxer.Duration;
                UIAdd(() =>
                {
                    IsLive  = IsLive;
                    Duration=Duration;
                });
            }

            UIAdd(() =>
            {
                LastError=LastError;
                Status  = Status;
            });

            UIAll();

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            OnOpenPlaylistItemCompleted(args);
        }
    }

    /// <summary>
    /// Opens a playlist item <see cref="Playlist.Items"/> without blocking
    /// You can get the results from <see cref="OpenPlaylistItemCompleted"/>
    /// </summary>
    /// <param name="item">The playlist item to open</param>
    /// <param name="defaultVideo">Whether to open the default video stream from plugin suggestions</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="defaultSubtitles">Whether to open the default subtitles stream from plugin suggestions</param>
    /// <returns></returns>
    public void OpenAsync(PlaylistItem item, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        => OpenAsyncPush(item, defaultVideo, defaultAudio, defaultSubtitles);

    /// <summary>
    /// Opens an external stream (audio/subtitles/video)
    /// </summary>
    /// <param name="extStream">The external stream to open</param>
    /// <param name="resync">Whether to force resync with other streams</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    /// <param name="streamIndex">-2: None, -1: Suggested/Default, X: Specified embedded stream index</param>
    /// <returns></returns>
    public ExternalStreamOpenedArgs Open(ExternalStream extStream, bool resync = true, bool defaultAudio = true, int streamIndex = -1)
    {
        /* TODO
         *
         * Decoder.Stop() should not be called on video input switch as it will close the other inputs as well (audio/subs)
         * If the input is from different plugin we don't dispose the current plugin (eg.  switching between recent/history plugin with torrents) (?)
         */

        ExternalStreamOpenedArgs args = null;

        try
        {
            int syncMs = decoder.GetCurTimeMs();

            if (LastError != null)
            {
                lastError = null;
                UI(() => LastError = LastError);
            }

            if (extStream is ExternalAudioStream)
            {
                if (decoder.VideoStream == null)
                    requiresBuffering = true;

                isAudioSwitch = true;
                Config.Audio.SetEnabled(true);
                args = decoder.Open(extStream, false, streamIndex);

                if (!args.Success)
                {
                    isAudioSwitch = false;
                    return args;
                }

                if (resync)
                    ReSync(decoder.AudioStream, syncMs);

                if (VideoDemuxer.VideoStream == null)
                {
                    isLive = MainDemuxer.IsLive;
                    duration = MainDemuxer.Duration;
                }

                isAudioSwitch = false;
            }
            else if (extStream is ExternalVideoStream)
            {
                bool shouldPlay = false;
                if (IsPlaying)
                {
                    shouldPlay = true;
                    Pause();
                }

                Initialize(Status.Opening, false, true);
                args = decoder.Open(extStream, defaultAudio, streamIndex);

                if (!args.Success || !CanPlay)
                    return args;

                decoder.Seek(syncMs, false, false);
                decoder.GetVideoFrame(syncMs * (long)10000);
                VideoDemuxer.Start();
                AudioDemuxer.Start();
                //for (int i = 0; i < subNum; i++)
                //{
                //    SubtitlesDemuxers[i].Start();
                //}
                DataDemuxer.Start();
                decoder.PauseOnQueueFull();

                // Initialize will Reset those and is posible that Codec Changed will not be called (as they are not chaning necessary)
                Decoder_OpenAudioStreamCompleted(null, null);
                Decoder_OpenSubtitlesStreamCompleted(null, null);

                if (shouldPlay)
                    Play();
                else
                    ShowOneFrame();
            }
            else // ExternalSubtitlesStream
            {
                if (!Video.IsOpened)
                {
                    args.Error = "Subtitles require opened video stream";
                    return args;
                }

                Config.Subtitles.SetEnabled(true);
                args = decoder.Open(extStream, false, streamIndex);
            }

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            if (extStream is ExternalVideoStream)
                OnOpenExternalVideoStreamCompleted((OpenExternalVideoStreamCompletedArgs)args);
            else if (extStream is ExternalAudioStream)
                OnOpenExternalAudioStreamCompleted((OpenExternalAudioStreamCompletedArgs)args);
            else
                OnOpenExternalSubtitlesStreamCompleted((OpenExternalSubtitlesStreamCompletedArgs)args);
        }
    }

    /// <summary>
    /// Opens an external stream (audio/subtitles/video) without blocking
    /// You can get the results from <see cref="OpenExternalAudioStreamCompleted"/>, <see cref="OpenExternalVideoStreamCompleted"/>, <see cref="OpenExternalSubtitlesStreamCompleted"/>
    /// </summary>
    /// <param name="extStream">The external stream to open</param>
    /// <param name="resync">Whether to force resync with other streams</param>
    /// <param name="defaultAudio">Whether to open the default audio stream from plugin suggestions</param>
    public void OpenAsync(ExternalStream extStream, bool resync = true, bool defaultAudio = true)
        => OpenAsyncPush(extStream, resync, defaultAudio);

    /// <summary>
    /// Opens an embedded stream (audio/subtitles/video)
    /// </summary>
    /// <param name="stream">An existing Player's media stream</param>
    /// <param name="resync">Whether to force resync with other streams</param>
    /// <param name="defaultAudio">Whether to re-suggest audio based on the new video stream (has effect only on VideoStream)</param>
    /// <returns></returns>
    public StreamOpenedArgs Open(StreamBase stream, bool resync = true, bool defaultAudio = true)
    {
        StreamOpenedArgs args = new();

        try
        {
            long delay = DateTime.UtcNow.Ticks;
            long fromEnd = Duration - CurTime;

            if (stream.Demuxer.Type == MediaType.Video)
            {
                isVideoSwitch = true;
                requiresBuffering = true;
            }

            if (stream is AudioStream astream)
            {
                Config.Audio.SetEnabled(true);
                args = decoder.OpenAudioStream(astream);
            }
            else if (stream is VideoStream vstream)
                args = decoder.OpenVideoStream(vstream, defaultAudio);
            else if (stream is SubtitlesStream sstream)
            {
                Config.Subtitles.SetEnabled(true);
                args = decoder.OpenSubtitlesStream(sstream);
            }
            else if (stream is DataStream dstream)
            {
                Config.Data.SetEnabled(true);
                args = decoder.OpenDataStream(dstream);
            }

            if (resync)
            {
                // Wait for at least on package before seek to update the HLS context first_time
                if (stream.Demuxer.IsHLSLive)
                {
                    while (stream.Demuxer.IsRunning && stream.Demuxer.GetPacketsPtr(stream).Count < 3)
                        System.Threading.Thread.Sleep(20);

                    ReSync(stream, (int) ((Duration - fromEnd - (DateTime.UtcNow.Ticks - delay))/ 10000));
                }
                else
                    ReSync(stream, (int) (CurTime / 10000), true);
            }
            else
                isVideoSwitch = false;

            return args;

        } catch (Exception e)
        {
            args.Error = !args.Success ? args.Error + "\r\n" + e.Message : e.Message;
            return args;
        } finally
        {
            if (stream is VideoStream)
                OnOpenVideoStreamCompleted((OpenVideoStreamCompletedArgs)args);
            else if (stream is AudioStream)
                OnOpenAudioStreamCompleted((OpenAudioStreamCompletedArgs)args);
            else if (stream is SubtitlesStream)
                OnOpenSubtitlesStreamCompleted((OpenSubtitlesStreamCompletedArgs)args);
            else
                OnOpenDataStreamCompleted((OpenDataStreamCompletedArgs)args);
        }
    }

    /// <summary>
    /// Opens an embedded stream (audio/subtitles/video) without blocking
    /// You can get the results from <see cref="OpenAudioStreamCompleted"/>, <see cref="OpenVideoStreamCompleted"/>, <see cref="OpenSubtitlesStreamCompleted"/>
    /// </summary>
    /// <param name="stream">An existing Player's media stream</param>
    /// <param name="resync">Whether to force resync with other streams</param>
    /// <param name="defaultAudio">Whether to re-suggest audio based on the new video stream (has effect only on VideoStream)</param>
    public void OpenAsync(StreamBase stream, bool resync = true, bool defaultAudio = true)
        => OpenAsyncPush(stream, resync, defaultAudio);

    /// <summary>
    /// Gets a session that can be re-opened later on with <see cref="Open(Session)"/>
    /// </summary>
    /// <param name="item">The current selected playlist item if null</param>
    /// <returns></returns>
    public Session GetSession(PlaylistItem item = null)
        => Playlist.Selected != null && (item == null || item.Index == Playlist.Selected.Index)
        ? GetCurrentSession()
        : item != null && item.GetTag(playerSessionTag) != null ? (Session)item.GetTag(playerSessionTag) : null;
    string playerSessionTag = "_session";
    private Session GetCurrentSession()
    {
        Session session = new();
        var item = Playlist.Selected;

        session.Url = Playlist.Url;
        session.PlaylistItem = item.Index;

        if (item.ExternalAudioStream != null)
            session.ExternalAudioStream = item.ExternalAudioStream.Index;

        if (item.ExternalVideoStream != null)
            session.ExternalVideoStream = item.ExternalVideoStream.Index;

        // TODO: L: Support secondary subtitles
        if (item.ExternalSubtitlesStreams[0] != null)
            session.ExternalSubtitlesUrl = item.ExternalSubtitlesStreams[0].Url;
        else if (decoder.SubtitlesStreams[0] != null)
            session.SubtitlesStream = decoder.SubtitlesStreams[0].StreamIndex;

        if (decoder.AudioStream != null)
            session.AudioStream = decoder.AudioStream.StreamIndex;

        if (decoder.VideoStream != null)
            session.VideoStream = decoder.VideoStream.StreamIndex;

        session.CurTime = CurTime;
        session.AudioDelay = Config.Audio.Delay;
        // TODO: L: Support secondary subtitles
        session.SubtitlesDelay = Config.Subtitles[0].Delay;

        return session;
    }

    internal void ReSync(StreamBase stream, int syncMs = -1, bool accurate = false)
    {
        /* TODO
         *
         * HLS live resync on stream switch should be from the end not from the start (could have different cache/duration)
         */

        if (stream == null) return;
        //if (stream == null || (syncMs == 0 || (syncMs == -1 && decoder.GetCurTimeMs() == 0))) return; // Avoid initial open resync?

        if (stream.Demuxer.Type == MediaType.Video)
        {
            isVideoSwitch = true;
            isAudioSwitch = true;
            for (int i = 0; i < subNum; i++)
            {
                isSubsSwitches[i] = true;
            }
            isDataSwitch = true;
            requiresBuffering = true;

            if (accurate && Video.IsOpened)
            {
                decoder.PauseDecoders();
                decoder.Seek(syncMs, false, false);
                decoder.GetVideoFrame(syncMs * (long)10000); // TBR: syncMs should not be -1 here
            }
            else
                decoder.Seek(syncMs, false, false);

            aFrame = null;
            isAudioSwitch = false;
            isVideoSwitch = false;
            for (int i = 0; i < subNum; i++)
            {
                sFrames[i] = sFramesPrev[i] = null;
                isSubsSwitches[i] = false;
            }
            dFrame = null;
            isDataSwitch = false;

            if (!IsPlaying)
            {
                decoder.PauseDecoders();
                decoder.GetVideoFrame();
                ShowOneFrame();
            }
            else
            {
                SubtitleClear();
            }
        }
        else
        {
            if (stream.Demuxer.Type == MediaType.Audio)
            {
                isAudioSwitch = true;
                decoder.SeekAudio();
                aFrame = null;
                isAudioSwitch = false;
            }
            else if (stream.Demuxer.Type == MediaType.Subs)
            {
                int i = SubtitlesSelectedHelper.CurIndex;

                isSubsSwitches[i] = true;

                //decoder.SeekSubtitles(i);
                sFrames[i] = sFramesPrev[i] = null;
                SubtitleClear(i);
                isSubsSwitches[i] = false;

                // do not start demuxer and decoder for external subs
                return;
            }
            else if (stream.Demuxer.Type == MediaType.Data)
            {
                isDataSwitch = true;
                decoder.SeekData();
                dFrame = null;
                isDataSwitch = false;
            }

            if (IsPlaying)
            {
                stream.Demuxer.Start();
                decoder.GetDecoderPtr(stream).Start();
            }
        }
    }
    #endregion

    #region OpenAsync Implementation
    private void OpenAsync()
    {
        lock (lockActions)
            if (taskOpenAsyncRuns)
                return;

        taskOpenAsyncRuns = true;

        Task.Run(() =>
        {
            if (IsDisposed)
                return;

            while (true)
            {
                if (openInputs.TryPop(out var data))
                {
                    openInputs.Clear();
                    decoder.Interrupt = true;
                    OpenInternal(data.url_iostream, data.defaultPlaylistItem, data.defaultVideo, data.defaultAudio, data.defaultSubtitles);
                }
                else if (openSessions.TryPop(out data))
                {
                    openSessions.Clear();
                    decoder.Interrupt = true;
                    Open(data.session);
                }
                else if (openItems.TryPop(out data))
                {
                    openItems.Clear();
                    decoder.Interrupt = true;
                    Open(data.playlistItem, data.defaultVideo, data.defaultAudio, data.defaultSubtitles);
                }
                else if (openVideo.TryPop(out data))
                {
                    openVideo.Clear();
                    if (data.extStream != null)
                        Open(data.extStream, data.resync, data.defaultAudio);
                    else
                        Open(data.stream, data.resync, data.defaultAudio);
                }
                else if (openAudio.TryPop(out data))
                {
                    openAudio.Clear();
                    if (data.extStream != null)
                        Open(data.extStream, data.resync);
                    else
                        Open(data.stream, data.resync);
                }
                else if (openSubtitles.TryPop(out data))
                {
                    openSubtitles.Clear();
                    if (data.url_iostream != null)
                        OpenSubtitles(data.url_iostream.ToString());
                    else if (data.extStream != null)
                        Open(data.extStream, data.resync);
                    else if (data.stream != null)
                        Open(data.stream, data.resync);
                }
                else
                {
                    lock (lockActions)
                    {
                        if (openInputs.IsEmpty && openSessions.IsEmpty && openItems.IsEmpty && openVideo.IsEmpty && openAudio.IsEmpty && openSubtitles.IsEmpty)
                        {
                            taskOpenAsyncRuns = false;
                            break;
                        }
                    }
                }
            }
        });
    }

    private void OpenAsyncPush(object url_iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        lock (lockActions)
        {
            if (url_iostream is string url_iostream_str)
            {
                // convert Windows lnk file to targetPath
                if (Path.GetExtension(url_iostream_str).Equals(".lnk", StringComparison.OrdinalIgnoreCase))
                {
                    var targetPath = GetLnkTargetPath(url_iostream_str);
                    if (targetPath != null)
                        url_iostream = targetPath;
                }
            }

            if ((url_iostream is string) && ExtensionsSubtitles.Contains(GetUrlExtention(url_iostream.ToString())))
            {
                OnOpening(new() { Url = url_iostream.ToString(), IsSubtitles = true});
                openSubtitles.Push(new OpenAsyncData(url_iostream));
            }
            else
            {
                decoder.Interrupt = true;

                if (url_iostream is string)
                    OnOpening(new() { Url = url_iostream.ToString() });
                else
                    OnOpening(new() { IOStream = (Stream)url_iostream });

                openInputs.Push(new OpenAsyncData(url_iostream, defaultPlaylistItem, defaultVideo, defaultAudio, defaultSubtitles));
            }

            // Select primary subtitle & original mode when dropped
            SubtitlesSelectedHelper.CurIndex = 0;
            SubtitlesSelectedHelper.PrimaryMethod = SelectSubMethod.Original;

            OpenAsync();
        }
    }
    private void OpenAsyncPush(Session session)
    {
        lock (lockActions)
        {
            if (!openInputs.IsEmpty)
                return;

            decoder.Interrupt = true;
            openSessions.Clear();
            openSessions.Push(new OpenAsyncData(session));

            OpenAsync();
        }
    }
    private void OpenAsyncPush(PlaylistItem playlistItem, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
    {
        lock (lockActions)
        {
            if (!openInputs.IsEmpty || !openSessions.IsEmpty)
                return;

            decoder.Interrupt = true;
            openItems.Push(new OpenAsyncData(playlistItem, defaultVideo, defaultAudio, defaultSubtitles));

            OpenAsync();
        }
    }
    private void OpenAsyncPush(ExternalStream extStream, bool resync = true, bool defaultAudio = true, int streamIndex = -1)
    {
        lock (lockActions)
        {
            if (!openInputs.IsEmpty || !openItems.IsEmpty || !openSessions.IsEmpty)
                return;

            if (extStream is ExternalAudioStream)
            {
                openAudio.Clear();
                openAudio.Push(new OpenAsyncData(extStream, resync, false, streamIndex));
            }
            else if (extStream is ExternalVideoStream)
            {
                openVideo.Clear();
                openVideo.Push(new OpenAsyncData(extStream, resync, defaultAudio, streamIndex));
            }
            else
            {
                openSubtitles.Clear();
                openSubtitles.Push(new OpenAsyncData(extStream, resync, false, streamIndex));
            }

            OpenAsync();
        }
    }
    private void OpenAsyncPush(StreamBase stream, bool resync = true, bool defaultAudio = true)
    {
        lock (lockActions)
        {
            if (!openInputs.IsEmpty || !openItems.IsEmpty || !openSessions.IsEmpty)
                return;

            if (stream is AudioStream)
            {
                openAudio.Clear();
                openAudio.Push(new OpenAsyncData(stream, resync));
            }
            else if (stream is VideoStream)
            {
                openVideo.Clear();
                openVideo.Push(new OpenAsyncData(stream, resync, defaultAudio));
            }
            else
            {
                openSubtitles.Clear();
                openSubtitles.Push(new OpenAsyncData(stream, resync));
            }

            OpenAsync();
        }
    }

    ConcurrentStack<OpenAsyncData> openInputs   = new();
    ConcurrentStack<OpenAsyncData> openSessions = new();
    ConcurrentStack<OpenAsyncData> openItems    = new();
    ConcurrentStack<OpenAsyncData> openVideo    = new();
    ConcurrentStack<OpenAsyncData> openAudio    = new();
    ConcurrentStack<OpenAsyncData> openSubtitles= new();
    #endregion
}

public class OpeningArgs
{
    public string       Url;
    public Stream       IOStream;
    public bool         IsSubtitles;
}

public class OpenCompletedArgs
{
    public string       Url;
    public Stream       IOStream;
    public string       Error;
    public bool         Success => Error == null;
    public bool         IsSubtitles;

    public OpenCompletedArgs(string url = null, Stream iostream = null, string error = null, bool isSubtitles = false) { Url = url; IOStream = iostream; Error = error; IsSubtitles = isSubtitles; }
}

class OpenAsyncData
{
    public object url_iostream;
    public Session session;
    public PlaylistItem playlistItem;
    public ExternalStream extStream;
    public int streamIndex;
    public StreamBase stream;
    public bool resync;
    public bool defaultPlaylistItem;
    public bool defaultAudio;
    public bool defaultVideo;
    public bool defaultSubtitles;

    public OpenAsyncData(object url_iostream, bool defaultPlaylistItem = true, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        { this.url_iostream = url_iostream; this.defaultPlaylistItem = defaultPlaylistItem; this.defaultVideo = defaultVideo; this.defaultAudio = defaultAudio; this.defaultSubtitles = defaultSubtitles; }
    public OpenAsyncData(Session session) => this.session = session;
    public OpenAsyncData(PlaylistItem playlistItem, bool defaultVideo = true, bool defaultAudio = true, bool defaultSubtitles = true)
        { this.playlistItem = playlistItem; this.defaultVideo = defaultVideo; this.defaultAudio = defaultAudio; this.defaultSubtitles = defaultSubtitles; }
    public OpenAsyncData(ExternalStream extStream, bool resync = true, bool defaultAudio = true, int streamIndex = -1)
        { this.extStream = extStream; this.resync = resync; this.defaultAudio = defaultAudio; this.streamIndex = streamIndex; }
    public OpenAsyncData(StreamBase stream, bool resync = true, bool defaultAudio = true)
        { this.stream = stream; this.resync = resync; this.defaultAudio = defaultAudio; }
}

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using FlyleafLib.MediaPlayer.Translation.Services;
using LLPlayer.Extensions;
using LLPlayer.Services;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitlesAction : UserControl
{
    public SettingsSubtitlesAction()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsSubtitlesActionVM>();
    }
}

public class SettingsSubtitlesActionVM : Bindable
{
    public FlyleafManager FL { get; }

    public SettingsSubtitlesActionVM(FlyleafManager fl)
    {
        FL = fl;

        SelectedTranslateWordServiceType = FL.PlayerConfig.Subtitles.TranslateWordServiceType;

        List<WordClickAction> wordClickActions = Enum.GetValues<WordClickAction>().ToList();
        if (string.IsNullOrEmpty(FL.Config.Subs.PDICPipeExecutablePath))
        {
            // PDIC is enabled only when exe is configured
            wordClickActions.Remove(WordClickAction.PDIC);
        }
        WordClickActions = wordClickActions;

        foreach (IMenuAction menuAction in FL.Config.Subs.WordMenuActions)
        {
            MenuActions.Add((IMenuAction)menuAction.Clone());
        }
    }

    public TranslateServiceType SelectedTranslateWordServiceType
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                FL.PlayerConfig.Subtitles.TranslateWordServiceType = value;
            }
        }
    }

    public List<WordClickAction> WordClickActions { get; }

    public List<ModifierKeys> ModifierKeys { get; } =
    [
        System.Windows.Input.ModifierKeys.Control,
        System.Windows.Input.ModifierKeys.Shift,
        System.Windows.Input.ModifierKeys.Alt,
        System.Windows.Input.ModifierKeys.None
    ];

    public ObservableCollection<IMenuAction> MenuActions { get; } = new();

    public DelegateCommand? CmdApplyContextMenu => field ??= new(() =>
    {
        ObservableCollection<IMenuAction> newActions = new(MenuActions.Select(a => (IMenuAction)a.Clone()));

        // Apply to config
        FL.Config.Subs.WordMenuActions = newActions;
    });

    public DelegateCommand? CmdAddSearchAction => field ??= new(() =>
    {
        MenuActions.Add(new SearchMenuAction
        {
            Title = "New Search",
            Url = "https://example.com/?q=%w"
        });
    });

    public DelegateCommand? CmdAddClipboardAction => field ??= new(() =>
    {
        MenuActions.Add(new ClipboardMenuAction());
    });

    public DelegateCommand? CmdAddClipboardAllAction => field ??= new(() =>
    {
        MenuActions.Add(new ClipboardAllMenuAction());
    });

    public DelegateCommand<IMenuAction>? CmdRemoveAction => field ??= new((action) =>
    {
        if (action != null)
        {
            MenuActions.Remove(action);
        }
    });

    // TODO: L: SaveCommand?
}

class DataGridRowOrderBehaviorMenuAction : DataGridRowOrderBehavior<IMenuAction>;

class MenuActionTemplateSelector : DataTemplateSelector
{
    public required DataTemplate SearchTemplate { get; set; }
    public required DataTemplate ClipboardTemplate { get; set; }
    public required DataTemplate ClipboardAllTemplate { get; set; }

    public override DataTemplate? SelectTemplate(object? item, DependencyObject container)
    {
        return item switch
        {
            SearchMenuAction => SearchTemplate,
            ClipboardMenuAction => ClipboardTemplate,
            ClipboardAllMenuAction => ClipboardAllTemplate,
            _ => null
        };
    }
}

using System.Threading;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;

using FlyleafLib.MediaFramework.MediaFrame;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaFramework.MediaDecoder;
public unsafe class DataDecoder : DecoderBase
{
    public DataStream DataStream => (DataStream)Stream;

    public ConcurrentQueue<DataFrame>
                            Frames              { get; protected set; } = new ConcurrentQueue<DataFrame>();

    public DataDecoder(Config config, int uniqueId = -1) : base(config, uniqueId) { }

    protected override unsafe int Setup(AVCodec* codec) => 0;
    protected override void DisposeInternal()
        => Frames = new ConcurrentQueue<DataFrame>();

    public void Flush()
    {
        lock (lockActions)
            lock (lockCodecCtx)
            {
                if (Disposed)
                    return;

                if (Status == Status.Ended)
                    Status = Status.Stopped;
                else if (Status == Status.Draining)
                    Status = Status.Stopping;

                DisposeFrames();
            }
    }

    protected override void RunInternal()
    {
        int allowedErrors = Config.Decoder.MaxErrors;
        AVPacket *packet;

        do
        {
            // Wait until Queue not Full or Stopped
            if (Frames.Count >= Config.Decoder.MaxDataFrames)
            {
                lock (lockStatus)
                    if (Status == Status.Running)
                        Status = Status.QueueFull;

                while (Frames.Count >= Config.Decoder.MaxDataFrames && Status == Status.QueueFull)
                    Thread.Sleep(20);

                lock (lockStatus)
                {
                    if (Status != Status.QueueFull)
                        break;
                    Status = Status.Running;
                }
            }

            // While Packets Queue Empty (Ended | Quit if Demuxer stopped | Wait until we get packets)
            if (demuxer.DataPackets.Count == 0)
            {
                CriticalArea = true;

                lock (lockStatus)
                    if (Status == Status.Running)
                        Status = Status.QueueEmpty;

                while (demuxer.DataPackets.Count == 0 && Status == Status.QueueEmpty)
                {
                    if (demuxer.Status == Status.Ended)
                    {
                        Status = Status.Draining;
                        break;
                    }
                    else if (!demuxer.IsRunning)
                    {
                        int retries = 5;

                        while (retries > 0)
                        {
                            retries--;
                            Thread.Sleep(10);
                            if (demuxer.IsRunning)
                                break;
                        }

                        lock (demuxer.lockStatus)
                            lock (lockStatus)
                            {
                                if (demuxer.Status == Status.Pausing || demuxer.Status == Status.Paused)
                                    Status = Status.Pausing;
                                else if (demuxer.Status == Status.QueueFull)
                                    Status = Status.Draining;
                                else if (demuxer.Status == Status.Running)
                                    continue;
                                else if (demuxer.Status != Status.Ended)
                                    Status = Status.Stopping;
                                else
                                    continue;
                            }

                        break;
                    }

                    Thread.Sleep(20);
                }

                lock (lockStatus)
                {
                    CriticalArea = false;
                    if (Status != Status.QueueEmpty && Status != Status.Draining)
                        break;
                    if (Status != Status.Draining)
                        Status = Status.Running;
                }
            }

            lock (lockCodecCtx)
            {
                if (Status == Status.Stopped || demuxer.DataPackets.Count == 0)
                    continue;
                packet = demuxer.DataPackets.Dequeue();

                if (packet->size > 0 && packet->data != null)
                {
                    var mFrame = ProcessDataFrame(packet);
                    Frames.Enqueue(mFrame);
                }

                av_packet_free(&packet);
            }
        } while (Status == Status.Running);

        if (Status == Status.Draining) Status = Status.Ended;
    }

    private DataFrame ProcessDataFrame(AVPacket* packet)
    {
        IntPtr ptr = new IntPtr(packet->data);
        byte[] dataFrame = new byte[packet->size];
        Marshal.Copy(ptr, dataFrame, 0, packet->size); // for performance/in case of large data we could just keep the avpacket data directly (DataFrame instead of byte[] Data just use AVPacket* and move ref?)

        DataFrame mFrame = new()
        {
            timestamp   = (long)(packet->pts * DataStream.Timebase) - demuxer.StartTime,
            DataCodecId = DataStream.CodecID,
            Data = dataFrame
        };

        return mFrame;
    }

    public void DisposeFrames()
        => Frames = new ConcurrentQueue<DataFrame>();
}

using System.Diagnostics;
using System.IO;
using System.Text;

namespace LLPlayer.Services;

public class PipeClient : IDisposable
{
    private Process _proc;

    public PipeClient(string pipePath)
    {
        _proc = new Process
        {
            StartInfo = new ProcessStartInfo()
            {
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                RedirectStandardError = true,
                FileName = pipePath,
                CreateNoWindow = true,
            }
        };
        _proc.Start();
    }

    public async Task SendMessage(string message)
    {
        Debug.WriteLine(message);
        //byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(message);

        // Enclose double quotes before and after since it is sent as a JSON string
        byte[] bytes = Encoding.UTF8.GetBytes('"' + message + '"');

        var length = BitConverter.GetBytes(bytes.Length);
        await _proc.StandardInput.BaseStream.WriteAsync(length, 0, length.Length);
        await _proc.StandardInput.BaseStream.WriteAsync(bytes, 0, bytes.Length);
        await _proc.StandardInput.BaseStream.FlushAsync();
    }

    public void Dispose()
    {
        _proc.Kill();
        _proc.WaitForExit();
    }
}

public class PDICSender : IDisposable
{
    private readonly PipeClient _pipeClient;
    public FlyleafManager FL { get; }

    public PDICSender(FlyleafManager fl)
    {
        FL = fl;

        string? exePath = FL.Config.Subs.PDICPipeExecutablePath;

        if (!File.Exists(exePath))
        {
            throw new FileNotFoundException($"PDIC executable is not set correctly: {exePath}");
        }

        _pipeClient = new PipeClient(exePath);
    }

    public async void Dispose()
    {
        await _pipeClient.SendMessage("p:Dictionary,Close,");
        _pipeClient.Dispose();
    }

    public async Task Connect()
    {
        await _pipeClient.SendMessage("p:Dictionary,Open,");
    }

    // Send the same way as Firepop
    // webextension native extensions
    // ref: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging

    // See Firepop source
    public async Task<int> SendWithPipe(string sentence, int offset)
    {
        try
        {
            await _pipeClient.SendMessage($"p:Dictionary,SetUrl,{App.Name}");
            await _pipeClient.SendMessage($"p:Dictionary,PopupSearch3,{offset},{sentence}");

            // Incremental search
            //await _pipeClient.SendMessage($"p:Simulate,InputWord3,word");

            return 0;
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.ToString());
            return -1;
        }
    }
}

using System.Diagnostics;
using System.Windows.Forms;

namespace FlyleafLib.MediaPlayer;

public class Activity : NotifyPropertyChanged
{
    /* Player Activity Mode ( Idle / Active / FullActive )
     *
     * Required Engine's Thread (UIRefresh)
     *
     * TODO: Static?
     */

    public ActivityMode Mode
        {
        get => mode;
        set {

            if (value == mode)
                return;

            mode = value;

            if (value == ActivityMode.Idle)
            {
                swKeyboard.Reset();
                swMouse.Reset();
            }
            else if (value == ActivityMode.Active)
                swKeyboard.Restart();
            else
                swMouse.Restart();

            Utils.UI(() => SetMode());
            }
        }
    internal ActivityMode _Mode = ActivityMode.FullActive, mode = ActivityMode.FullActive;

    /// <summary>
    /// Should use Timeout to Enable/Disable it. Use this only for temporary disable.
    /// </summary>
    public bool IsEnabled           { get => _IsEnabled;
        set {

            if (value && _Timeout <= 0)
            {
                if (_IsEnabled)
                {
                    _IsEnabled = false;
                    RaiseUI(nameof(IsEnabled));
                }
                else
                    _IsEnabled = false;

            }
            else
            {
                if (_IsEnabled == value)
                    return;

                if (value)
                {
                    swKeyboard.Restart();
                    swMouse.Restart();
                }

                _IsEnabled = value;
                RaiseUI(nameof(IsEnabled));
            }
            }
        }
    bool _IsEnabled;

    public int  Timeout             { get => _Timeout; set { _Timeout = value; IsEnabled = value > 0; } }
    int _Timeout;

    Player player;
    Stopwatch swKeyboard = new();
    Stopwatch swMouse = new();

    public Activity(Player player) => this.player = player;

    /// <summary>
    /// Updates Mode UI value and shows/hides mouse cursor if required
    /// Must be called from a UI Thread
    /// </summary>
    internal void SetMode()
    {
        _Mode = mode;
        Raise(nameof(Mode));
        player.Log.Trace(mode.ToString());

        if (player.Activity.Mode == ActivityMode.Idle && player.Host != null /*&& player.Host.Player_GetFullScreen() */&& player.Host.Player_CanHideCursor())
        {
            lock (cursorLocker)
            {
                while (Utils.NativeMethods.ShowCursor(false) >= 0) { }
                isCursorHidden = true;
            }

        }
        else if (isCursorHidden && player.Activity.Mode == ActivityMode.FullActive)
        {
            lock (cursorLocker)
            {
                while (Utils.NativeMethods.ShowCursor(true) < 0) { }
                isCursorHidden = false;
            }
        }
    }

    /// <summary>
    /// Refreshes mode value based on current timestamps
    /// </summary>
    internal void RefreshMode()
    {
        if (!IsEnabled)
            mode = ActivityMode.FullActive;
        else mode = swMouse.IsRunning && swMouse.ElapsedMilliseconds < Timeout
            ? ActivityMode.FullActive
            : swKeyboard.IsRunning && swKeyboard.ElapsedMilliseconds < Timeout ? ActivityMode.Active : ActivityMode.Idle;
    }

    /// <summary>
    /// Sets Mode to Idle
    /// </summary>
    public void ForceIdle()
    {
        if (Timeout > 0)
            Mode = ActivityMode.Idle;
    }
    /// <summary>
    /// Sets Mode to Active
    /// </summary>
    public void ForceActive()       => Mode = ActivityMode.Active;
    /// <summary>
    /// Sets Mode to Full Active
    /// </summary>
    public void ForceFullActive()   => Mode = ActivityMode.FullActive;

    /// <summary>
    /// Updates Active Timestamp
    /// </summary>
    public void RefreshActive()     => swKeyboard.Restart();

    /// <summary>
    /// Updates Full Active Timestamp
    /// </summary>
    public void RefreshFullActive() => swMouse.Restart();

    #region Ensures we catch the mouse move even when the Cursor is hidden
    static bool isCursorHidden;
    static object cursorLocker = new();
    public class GlobalMouseHandler : IMessageFilter
    {
        public bool PreFilterMessage(ref Message m)
        {
            if (isCursorHidden && m.Msg == 0x0200)
            {
                try
                {
                    lock (cursorLocker)
                    {
                        while (Utils.NativeMethods.ShowCursor(true) < 0) { }
                        isCursorHidden = false;
                        foreach(var player in Engine.Players)
                            player.Activity.RefreshFullActive();
                    }

                } catch { }
            }

            return false;
        }
    }
    static Activity()
    {
        GlobalMouseHandler gmh = new();
        Application.AddMessageFilter(gmh);
    }
    #endregion
}

public enum ActivityMode
{
    Idle,
    Active,     // Keyboard only
    FullActive  // Mouse
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace WpfColorFontDialog
{
    internal static class I18NUtil
    {
        private const string DefaultLanguage = @"en-US";

        public static string CurrentLanguage;

        public static readonly Dictionary<string, string> SupportLanguage = new Dictionary<string, string>
        {
            {@"简体中文", @"zh-CN"},
            {@"English (United States)", @"en-US"},
        };

        public static string GetCurrentLanguage()
        {
            return System.Globalization.CultureInfo.CurrentCulture.Name;
        }

        public static string GetLanguage()
        {
            var name = GetCurrentLanguage();
            return SupportLanguage.Any(s => name == s.Value) ? name : DefaultLanguage;
        }

        public static string GetWindowStringValue(Window window, string key)
        {
            if (window.Resources.MergedDictionaries[0][key] is string str)
            {
                return str;
            }
            return null;
        }

        public static void SetLanguage(ResourceDictionary resources, string langName = @"")
        {
            if (string.IsNullOrEmpty(langName))
            {
                langName = GetLanguage();
            }
            CurrentLanguage = langName;
            if (resources.MergedDictionaries.Count > 0)
            {
                resources.MergedDictionaries[0].Source = new Uri($@"I18n/{langName}.xaml", UriKind.Relative);
            }
        }
    }
}

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Media;

namespace LLPlayer.Extensions;

// Convert Color object to HEX string
public class ColorHexJsonConverter : JsonConverter<Color>
{
    public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
    {
        string hex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}";
        writer.WriteStringValue(hex);
    }

    public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string? hex = null;
        try
        {
            hex = reader.GetString();

            if (string.IsNullOrWhiteSpace(hex))
            {
                throw new JsonException("Color value is null or empty.");
            }

            if (!hex.StartsWith("#") || (hex.Length != 7 && hex.Length != 9))
            {
                throw new JsonException($"Invalid color format: {hex}");
            }
            byte a = 255;

            int start = 1;

            if (hex.Length == 9)
            {
                a = byte.Parse(hex.Substring(1, 2), NumberStyles.HexNumber);
                start = 3;
            }

            byte r = byte.Parse(hex.Substring(start, 2), NumberStyles.HexNumber);
            byte g = byte.Parse(hex.Substring(start + 2, 2), NumberStyles.HexNumber);
            byte b = byte.Parse(hex.Substring(start + 4, 2), NumberStyles.HexNumber);

            return Color.FromArgb(a, r, g, b);
        }
        catch (Exception ex)
        {
            throw new JsonException($"Error parsing color value: {hex}", ex);
        }
    }
}

using FlyleafLib.MediaFramework.MediaDemuxer;

namespace FlyleafLib.MediaFramework.MediaStream;

public unsafe class AudioStream : StreamBase
{
    public int              Bits                { get; set; }
    public int              Channels            { get; set; }
    public ulong            ChannelLayout       { get; set; }
    public string           ChannelLayoutStr    { get; set; }
    public AVSampleFormat   SampleFormat        { get; set; }
    public string           SampleFormatStr     { get; set; }
    public int              SampleRate          { get; set; }
    public AVCodecID        CodecIDOrig         { get; set; }

    public override string GetDump()
        => $"[{Type} #{StreamIndex}-{Language.IdSubLanguage}{(Title != null ? "(" + Title + ")" : "")}] {Codec} {SampleFormatStr}@{Bits} {SampleRate / 1000}KHz {ChannelLayoutStr} | [BR: {BitRate}] | {Utils.TicksToTime((long)(AVStream->start_time * Timebase))}/{Utils.TicksToTime((long)(AVStream->duration * Timebase))} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}";

    public AudioStream() { }
    public AudioStream(Demuxer demuxer, AVStream* st) : base(demuxer, st) => Refresh();

    public override void Refresh()
    {
        base.Refresh();

        SampleFormat    = (AVSampleFormat) Enum.ToObject(typeof(AVSampleFormat), AVStream->codecpar->format);
        SampleFormatStr = av_get_sample_fmt_name(SampleFormat);
        SampleRate      = AVStream->codecpar->sample_rate;

        if (AVStream->codecpar->ch_layout.order == AVChannelOrder.Unspec)
            av_channel_layout_default(&AVStream->codecpar->ch_layout, AVStream->codecpar->ch_layout.nb_channels);

        ChannelLayout   = AVStream->codecpar->ch_layout.u.mask;
        Channels        = AVStream->codecpar->ch_layout.nb_channels;
        Bits            = AVStream->codecpar->bits_per_coded_sample;

        // https://trac.ffmpeg.org/ticket/7321
        CodecIDOrig = CodecID;
        if (CodecID == AVCodecID.Mp2 && (SampleFormat == AVSampleFormat.Fltp || SampleFormat == AVSampleFormat.Flt))
            CodecID = AVCodecID.Mp3; // OR? st->codecpar->format = (int) AVSampleFormat.AV_SAMPLE_FMT_S16P;

        byte[] buf = new byte[50];
        fixed (byte* bufPtr = buf)
        {
            av_channel_layout_describe(&AVStream->codecpar->ch_layout, bufPtr, (nuint)buf.Length);
            ChannelLayoutStr = Utils.BytePtrToStringUTF8(bufPtr);
        }
    }
}

using FlyleafLib.MediaPlayer.Translation.Services;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

namespace FlyleafLib.MediaPlayer.Translation;

public class TranslateLanguage
{
    public static Dictionary<string, TranslateLanguage> Langs { get; } = new()
    {
        // Google: https://cloud.google.com/translate/docs/languages
        // DeepL: https://developers.deepl.com/docs/resources/supported-languages
        ["ab"] = new("Abkhaz", "ab", TranslateServiceType.GoogleV1),
        ["af"] = new("Afrikaans", "af", TranslateServiceType.GoogleV1),
        ["sq"] = new("Albanian", "sq", TranslateServiceType.GoogleV1),
        ["am"] = new("Amharic", "am", TranslateServiceType.GoogleV1),
        ["ar"] = new("Arabic", "ar"),
        ["hy"] = new("Armenian", "hy", TranslateServiceType.GoogleV1),
        ["as"] = new("Assamese", "as", TranslateServiceType.GoogleV1),
        ["ay"] = new("Aymara", "ay", TranslateServiceType.GoogleV1),
        ["az"] = new("Azerbaijani", "az", TranslateServiceType.GoogleV1),
        ["bm"] = new("Bambara", "bm", TranslateServiceType.GoogleV1),
        ["ba"] = new("Bashkir", "ba", TranslateServiceType.GoogleV1),
        ["eu"] = new("Basque", "eu", TranslateServiceType.GoogleV1),
        ["be"] = new("Belarusian", "be", TranslateServiceType.GoogleV1),
        ["bn"] = new("Bengali", "bn", TranslateServiceType.GoogleV1),
        ["bs"] = new("Bosnian", "bs", TranslateServiceType.GoogleV1),
        ["br"] = new("Breton", "br", TranslateServiceType.GoogleV1),
        ["bg"] = new("Bulgarian", "bg"),
        ["ca"] = new("Catalan", "ca", TranslateServiceType.GoogleV1),
        ["ny"] = new("Chichewa (Nyanja)", "ny", TranslateServiceType.GoogleV1),

        // special handling
        ["zh"] = new("Chinese", "zh"),

        ["cv"] = new("Chuvash", "cv", TranslateServiceType.GoogleV1),
        ["co"] = new("Corsican", "co", TranslateServiceType.GoogleV1),
        ["hr"] = new("Croatian", "hr", TranslateServiceType.GoogleV1),
        ["cs"] = new("Czech", "cs"),
        ["da"] = new("Danish", "da"),
        ["dv"] = new("Divehi", "dv", TranslateServiceType.GoogleV1),
        ["nl"] = new("Dutch", "nl"),
        ["dz"] = new("Dzongkha", "dz", TranslateServiceType.GoogleV1),
        ["en"] = new("English", "en"),
        ["eo"] = new("Esperanto", "eo", TranslateServiceType.GoogleV1),
        ["et"] = new("Estonian", "et"),
        ["ee"] = new("Ewe", "ee", TranslateServiceType.GoogleV1),
        ["fj"] = new("Fijian", "fj", TranslateServiceType.GoogleV1),
        ["tl"] = new("Tagalog", "tl", TranslateServiceType.GoogleV1),
        ["fi"] = new("Finnish", "fi"),

        // special handling
        ["fr"] = new("French", "fr"),

        ["fy"] = new("Frisian", "fy", TranslateServiceType.GoogleV1),
        ["ff"] = new("Fulfulde", "ff", TranslateServiceType.GoogleV1),
        ["gl"] = new("Galician", "gl", TranslateServiceType.GoogleV1),
        ["lg"] = new("Ganda (Luganda)", "lg", TranslateServiceType.GoogleV1),
        ["ka"] = new("Georgian", "ka", TranslateServiceType.GoogleV1),
        ["de"] = new("German", "de"),
        ["el"] = new("Greek", "el"),
        ["gn"] = new("Guarani", "gn", TranslateServiceType.GoogleV1),
        ["gu"] = new("Gujarati", "gu", TranslateServiceType.GoogleV1),
        ["ht"] = new("Haitian Creole", "ht", TranslateServiceType.GoogleV1),
        ["ha"] = new("Hausa", "ha", TranslateServiceType.GoogleV1),
        ["he"] = new("Hebrew", "he", TranslateServiceType.GoogleV1),
        ["hi"] = new("Hindi", "hi", TranslateServiceType.GoogleV1),
        ["hu"] = new("Hungarian", "hu"),
        ["is"] = new("Icelandic", "is", TranslateServiceType.GoogleV1),
        ["ig"] = new("Igbo", "ig", TranslateServiceType.GoogleV1),
        ["id"] = new("Indonesian", "id"),
        ["ga"] = new("Irish", "ga", TranslateServiceType.GoogleV1),
        ["it"] = new("Italian", "it"),
        ["ja"] = new("Japanese", "ja"),
        ["jv"] = new("Javanese", "jv", TranslateServiceType.GoogleV1),
        ["kn"] = new("Kannada", "kn", TranslateServiceType.GoogleV1),
        ["kk"] = new("Kazakh", "kk", TranslateServiceType.GoogleV1),
        ["km"] = new("Khmer", "km", TranslateServiceType.GoogleV1),
        ["rw"] = new("Kinyarwanda", "rw", TranslateServiceType.GoogleV1),
        ["ko"] = new("Korean", "ko"),
        ["ku"] = new("Kurdish (Kurmanji)", "ku", TranslateServiceType.GoogleV1),
        ["ky"] = new("Kyrgyz", "ky", TranslateServiceType.GoogleV1),
        ["lo"] = new("Lao", "lo", TranslateServiceType.GoogleV1),
        ["la"] = new("Latin", "la", TranslateServiceType.GoogleV1),
        ["lv"] = new("Latvian", "lv"),
        ["li"] = new("Limburgan", "li", TranslateServiceType.GoogleV1),
        ["ln"] = new("Lingala", "ln", TranslateServiceType.GoogleV1),
        ["lt"] = new("Lithuanian", "lt"),
        ["lb"] = new("Luxembourgish", "lb", TranslateServiceType.GoogleV1),
        ["mk"] = new("Macedonian", "mk", TranslateServiceType.GoogleV1),
        ["mg"] = new("Malagasy", "mg", TranslateServiceType.GoogleV1),
        ["ms"] = new("Malay", "ms", TranslateServiceType.GoogleV1),
        ["ml"] = new("Malayalam", "ml", TranslateServiceType.GoogleV1),
        ["mt"] = new("Maltese", "mt", TranslateServiceType.GoogleV1),
        ["mi"] = new("Maori", "mi", TranslateServiceType.GoogleV1),
        ["mr"] = new("Marathi", "mr", TranslateServiceType.GoogleV1),
        ["mn"] = new("Mongolian", "mn", TranslateServiceType.GoogleV1),
        ["my"] = new("Myanmar (Burmese)", "my", TranslateServiceType.GoogleV1),
        ["nr"] = new("Ndebele (South)", "nr", TranslateServiceType.GoogleV1),
        ["ne"] = new("Nepali", "ne", TranslateServiceType.GoogleV1),

        // TODO: L: review handling
        // special handling
        ["no"] = new("Norwegian", "no"),
        ["nb"] = new("Norwegian Bokmål", "nb"),

        ["oc"] = new("Occitan", "oc", TranslateServiceType.GoogleV1),
        ["or"] = new("Odia (Oriya)", "or", TranslateServiceType.GoogleV1),
        ["om"] = new("Oromo", "om", TranslateServiceType.GoogleV1),
        ["ps"] = new("Pashto", "ps", TranslateServiceType.GoogleV1),
        ["fa"] = new("Persian", "fa", TranslateServiceType.GoogleV1),
        ["pl"] = new("Polish", "pl"),

        // special handling
        ["pt"] = new("Portuguese", "pt"),

        ["pa"] = new("Punjabi", "pa", TranslateServiceType.GoogleV1),
        ["qu"] = new("Quechua", "qu", TranslateServiceType.GoogleV1),
        ["ro"] = new("Romanian", "ro"),
        ["rn"] = new("Rundi", "rn", TranslateServiceType.GoogleV1),
        ["ru"] = new("Russian", "ru"),
        ["sm"] = new("Samoan", "sm", TranslateServiceType.GoogleV1),
        ["sg"] = new("Sango", "sg", TranslateServiceType.GoogleV1),
        ["sa"] = new("Sanskrit", "sa", TranslateServiceType.GoogleV1),
        ["gd"] = new("Scots Gaelic", "gd", TranslateServiceType.GoogleV1),
        ["sr"] = new("Serbian", "sr", TranslateServiceType.GoogleV1),
        ["st"] = new("Sesotho", "st", TranslateServiceType.GoogleV1),
        ["sn"] = new("Shona", "sn", TranslateServiceType.GoogleV1),
        ["sd"] = new("Sindhi", "sd", TranslateServiceType.GoogleV1),
        ["si"] = new("Sinhala (Sinhalese)", "si", TranslateServiceType.GoogleV1),
        ["sk"] = new("Slovak", "sk"),
        ["sl"] = new("Slovenian", "sl"),
        ["so"] = new("Somali", "so", TranslateServiceType.GoogleV1),
        ["es"] = new("Spanish", "es"),
        ["su"] = new("Sundanese", "su", TranslateServiceType.GoogleV1),
        ["sw"] = new("Swahili", "sw", TranslateServiceType.GoogleV1),
        ["ss"] = new("Swati", "ss", TranslateServiceType.GoogleV1),
        ["sv"] = new("Swedish", "sv"),
        ["tg"] = new("Tajik", "tg", TranslateServiceType.GoogleV1),
        ["ta"] = new("Tamil", "ta", TranslateServiceType.GoogleV1),
        ["tt"] = new("Tatar", "tt", TranslateServiceType.GoogleV1),
        ["te"] = new("Telugu", "te", TranslateServiceType.GoogleV1),
        ["th"] = new("Thai", "th", TranslateServiceType.GoogleV1),
        ["ti"] = new("Tigrinya", "ti", TranslateServiceType.GoogleV1),
        ["ts"] = new("Tsonga", "ts", TranslateServiceType.GoogleV1),
        ["tn"] = new("Tswana", "tn", TranslateServiceType.GoogleV1),
        ["tr"] = new("Turkish", "tr"),
        ["tk"] = new("Turkmen", "tk", TranslateServiceType.GoogleV1),
        ["ak"] = new("Twi (Akan)", "ak", TranslateServiceType.GoogleV1),
        ["uk"] = new("Ukrainian", "uk"),
        ["ur"] = new("Urdu", "ur", TranslateServiceType.GoogleV1),
        ["ug"] = new("Uyghur", "ug", TranslateServiceType.GoogleV1),
        ["uz"] = new("Uzbek", "uz", TranslateServiceType.GoogleV1),
        ["vi"] = new("Vietnamese", "vi", TranslateServiceType.GoogleV1),
        ["cy"] = new("Welsh", "cy", TranslateServiceType.GoogleV1),
        ["xh"] = new("Xhosa", "xh", TranslateServiceType.GoogleV1),
        ["yi"] = new("Yiddish", "yi", TranslateServiceType.GoogleV1),
        ["yo"] = new("Yoruba", "yo", TranslateServiceType.GoogleV1),
        ["zu"] = new("Zulu", "zu", TranslateServiceType.GoogleV1),
    };

    public TranslateLanguage(string name, string iso6391,
        TranslateServiceType supportedServices =
            TranslateServiceType.GoogleV1 | TranslateServiceType.DeepL)
    {
        // all LLMs support all languages
        supportedServices |= TranslateServiceTypeExtensions.LLMServices;

        // DeepL = DeepLX, so flag is same
        if (supportedServices.HasFlag(TranslateServiceType.DeepL))
        {
            supportedServices |= TranslateServiceType.DeepLX;
        }

        Name = name;
        ISO6391 = iso6391;
        SupportedServices = supportedServices;
    }

    public string Name { get; }

    public string ISO6391 { get; }

    public TranslateServiceType SupportedServices { get; }
}

[DataContract]
public enum TargetLanguage
{
    // Supported by Google and DeepL
    [EnumMember(Value = "ar")] Arabic,
    [EnumMember(Value = "bg")] Bulgarian,
    [EnumMember(Value = "zh-CN")] ChineseSimplified,
    [EnumMember(Value = "zh-TW")] ChineseTraditional,
    [EnumMember(Value = "cs")] Czech,
    [EnumMember(Value = "da")] Danish,
    [EnumMember(Value = "nl")] Dutch,
    [EnumMember(Value = "en-US")] EnglishAmerican,
    [EnumMember(Value = "en-GB")] EnglishBritish,
    [EnumMember(Value = "et")] Estonian,
    [EnumMember(Value = "fr-FR")] French,
    [EnumMember(Value = "fr-CA")] FrenchCanadian,
    [EnumMember(Value = "de")] German,
    [EnumMember(Value = "el")] Greek,
    [EnumMember(Value = "hu")] Hungarian,
    [EnumMember(Value = "id")] Indonesian,
    [EnumMember(Value = "it")] Italian,
    [EnumMember(Value = "ja")] Japanese,
    [EnumMember(Value = "ko")] Korean,
    [EnumMember(Value = "lt")] Lithuanian,
    [EnumMember(Value = "nb")] NorwegianBokmål,
    [EnumMember(Value = "pl")] Polish,
    [EnumMember(Value = "pt-PT")] Portuguese,
    [EnumMember(Value = "pt-BR")] PortugueseBrazilian,
    [EnumMember(Value = "ro")] Romanian,
    [EnumMember(Value = "ru")] Russian,
    [EnumMember(Value = "sk")] Slovak,
    [EnumMember(Value = "sl")] Slovenian,
    [EnumMember(Value = "es")] Spanish,
    [EnumMember(Value = "sv")] Swedish,
    [EnumMember(Value = "tr")] Turkish,
    [EnumMember(Value = "uk")] Ukrainian,

    // Only supported in Google
    [EnumMember(Value = "ab")] Abkhaz,
    [EnumMember(Value = "af")] Afrikaans,
    [EnumMember(Value = "sq")] Albanian,
    [EnumMember(Value = "am")] Amharic,
    [EnumMember(Value = "hy")] Armenian,
    [EnumMember(Value = "as")] Assamese,
    [EnumMember(Value = "ay")] Aymara,
    [EnumMember(Value = "az")] Azerbaijani,
    [EnumMember(Value = "bm")] Bambara,
    [EnumMember(Value = "ba")] Bashkir,
    [EnumMember(Value = "eu")] Basque,
    [EnumMember(Value = "be")] Belarusian,
    [EnumMember(Value = "bn")] Bengali,
    [EnumMember(Value = "bs")] Bosnian,
    [EnumMember(Value = "br")] Breton,
    [EnumMember(Value = "ca")] Catalan,
    [EnumMember(Value = "ny")] Chichewa,
    [EnumMember(Value = "cv")] Chuvash,
    [EnumMember(Value = "co")] Corsican,
    [EnumMember(Value = "hr")] Croatian,
    [EnumMember(Value = "dv")] Divehi,
    [EnumMember(Value = "dz")] Dzongkha,
    [EnumMember(Value = "eo")] Esperanto,
    [EnumMember(Value = "ee")] Ewe,
    [EnumMember(Value = "fj")] Fijian,
    [EnumMember(Value = "tl")] Tagalog,
    [EnumMember(Value = "fy")] Frisian,
    [EnumMember(Value = "ff")] Fulfulde,
    [EnumMember(Value = "gl")] Galician,
    [EnumMember(Value = "lg")] Ganda,
    [EnumMember(Value = "ka")] Georgian,
    [EnumMember(Value = "gn")] Guarani,
    [EnumMember(Value = "gu")] Gujarati,
    [EnumMember(Value = "ht")] Haitian,
    [EnumMember(Value = "ha")] Hausa,
    [EnumMember(Value = "he")] Hebrew,
    [EnumMember(Value = "hi")] Hindi,
    [EnumMember(Value = "is")] Icelandic,
    [EnumMember(Value = "ig")] Igbo,
    [EnumMember(Value = "ga")] Irish,
    [EnumMember(Value = "jv")] Javanese,
    [EnumMember(Value = "kn")] Kannada,
    [EnumMember(Value = "kk")] Kazakh,
    [EnumMember(Value = "km")] Khmer,
    [EnumMember(Value = "rw")] Kinyarwanda,
    [EnumMember(Value = "ku")] Kurdish,
    [EnumMember(Value = "ky")] Kyrgyz,
    [EnumMember(Value = "lo")] Lao,
    [EnumMember(Value = "la")] Latin,
    [EnumMember(Value = "li")] Limburgan,
    [EnumMember(Value = "ln")] Lingala,
    [EnumMember(Value = "lb")] Luxembourgish,
    [EnumMember(Value = "mk")] Macedonian,
    [EnumMember(Value = "mg")] Malagasy,
    [EnumMember(Value = "ms")] Malay,
    [EnumMember(Value = "ml")] Malayalam,
    [EnumMember(Value = "mt")] Maltese,
    [EnumMember(Value = "mi")] Maori,
    [EnumMember(Value = "mr")] Marathi,
    [EnumMember(Value = "mn")] Mongolian,
    [EnumMember(Value = "my")] Myanmar,
    [EnumMember(Value = "nr")] Ndebele,
    [EnumMember(Value = "ne")] Nepali,
    [EnumMember(Value = "oc")] Occitan,
    [EnumMember(Value = "or")] Odia,
    [EnumMember(Value = "om")] Oromo,
    [EnumMember(Value = "ps")] Pashto,
    [EnumMember(Value = "fa")] Persian,
    [EnumMember(Value = "pa")] Punjabi,
    [EnumMember(Value = "qu")] Quechua,
    [EnumMember(Value = "rn")] Rundi,
    [EnumMember(Value = "sm")] Samoan,
    [EnumMember(Value = "sg")] Sango,
    [EnumMember(Value = "sa")] Sanskrit,
    [EnumMember(Value = "gd")] ScotsGaelic,
    [EnumMember(Value = "sr")] Serbian,
    [EnumMember(Value = "st")] Sesotho,
    [EnumMember(Value = "sn")] Shona,
    [EnumMember(Value = "sd")] Sindhi,
    [EnumMember(Value = "si")] Sinhala,
    [EnumMember(Value = "so")] Somali,
    [EnumMember(Value = "su")] Sundanese,
    [EnumMember(Value = "sw")] Swahili,
    [EnumMember(Value = "ss")] Swati,
    [EnumMember(Value = "tg")] Tajik,
    [EnumMember(Value = "ta")] Tamil,
    [EnumMember(Value = "tt")] Tatar,
    [EnumMember(Value = "te")] Telugu,
    [EnumMember(Value = "th")] Thai,
    [EnumMember(Value = "ti")] Tigrinya,
    [EnumMember(Value = "ts")] Tsonga,
    [EnumMember(Value = "tn")] Tswana,
    [EnumMember(Value = "tk")] Turkmen,
    [EnumMember(Value = "ak")] Twi,
    [EnumMember(Value = "ur")] Urdu,
    [EnumMember(Value = "ug")] Uyghur,
    [EnumMember(Value = "uz")] Uzbek,
    [EnumMember(Value = "vi")] Vietnamese,
    [EnumMember(Value = "cy")] Welsh,
    [EnumMember(Value = "xh")] Xhosa,
    [EnumMember(Value = "yi")] Yiddish,
    [EnumMember(Value = "yo")] Yoruba,
    [EnumMember(Value = "zu")] Zulu,
}

public static class TargetLanguageExtensions
{
    public static string DisplayName(this TargetLanguage targetLang)
    {
        return targetLang switch
        {
            TargetLanguage.EnglishAmerican => "English (American)",
            TargetLanguage.EnglishBritish => "English (British)",
            TargetLanguage.French => "French",
            TargetLanguage.FrenchCanadian => "French (Canadian)",
            TargetLanguage.Portuguese => "Portuguese",
            TargetLanguage.PortugueseBrazilian => "Portuguese (Brazilian)",
            TargetLanguage.ChineseSimplified => "Chinese (Simplified)",
            TargetLanguage.ChineseTraditional => "Chinese (Traditional)",
            TargetLanguage.NorwegianBokmål => "Norwegian Bokmål",

            TargetLanguage.Chichewa => "Chichewa (Nyanja)",
            TargetLanguage.Ganda => "Ganda (Luganda)",
            TargetLanguage.Haitian => "Haitian Creole",
            TargetLanguage.Kurdish => "Kurdish (Kurmanji)",
            TargetLanguage.Myanmar => "Myanmar (Burmese)",
            TargetLanguage.Ndebele => "Ndebele (South)",
            TargetLanguage.Odia => "Odia (Oriya)",
            TargetLanguage.ScotsGaelic => "Scots Gaelic",
            TargetLanguage.Sinhala => "Sinhala (Sinhalese)",
            TargetLanguage.Twi => "Twi (Akan)",
            _ => targetLang.ToString()
        };
    }

    public static TranslateServiceType SupportedServiceType(this TargetLanguage targetLang)
    {
        string iso6391 = targetLang.ToISO6391();

        TranslateLanguage lang = TranslateLanguage.Langs[iso6391];

        return lang.SupportedServices;
    }

    public static string ToISO6391(this TargetLanguage targetLang)
    {
        // Get EnumMember language code
        Type type = targetLang.GetType();
        MemberInfo[] memInfo = type.GetMember(targetLang.ToString());
        object[] attributes = memInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false);
        string enumValue = ((EnumMemberAttribute)attributes[0]).Value;

        // ISO6391 code is the first half of a hyphen
        if (enumValue != null && enumValue.Contains("-"))
        {
            return enumValue.Split('-')[0];
        }

        return enumValue;
    }

    public static TargetLanguage? ToTargetLanguage(this Language lang)
    {
        // language with region first
        switch (lang.ISO6391)
        {
            case "en" when lang.CultureName == "en-GB":
                return TargetLanguage.EnglishBritish;
            case "en":
                return TargetLanguage.EnglishAmerican;

            case "zh" when lang.CultureName.StartsWith("zh-Hant"): // zh-Hant, zh-Hant-XX
                return TargetLanguage.ChineseTraditional;
            case "zh": // zh-Hans, zh-Hans-XX, or others
                return TargetLanguage.ChineseSimplified;

            case "fr" when lang.CultureName == "fr-CA":
                return TargetLanguage.FrenchCanadian;
            case "fr":
                return TargetLanguage.French;

            case "pt" when lang.CultureName == "pt-BR":
                return TargetLanguage.PortugueseBrazilian;
            case "pt":
                return TargetLanguage.Portuguese;
        }

        // other languages with no region
        foreach (var tl in Enum.GetValues<TargetLanguage>())
        {
            if (tl.ToISO6391() == lang.ISO6391)
            {
                return tl;
            }
        }

        // not match
        return null;
    }
}

using System;
using System.IO;

using FlyleafLib.MediaFramework.MediaPlaylist;

namespace FlyleafLib.Plugins;

public class OpenDefault : PluginBase, IOpen, IScrapeItem
{
    /* TODO
     *
     * 1) Current Url Syntax issues
     *  ..\..\..\..\folder\file.mp3 | Cannot handle this
     *  file:///C:/folder/fi%20le.mp3 | FFmpeg & File.Exists cannot handle this
     *
     */

    public new int  Priority    { get; set; } = 3000;

    public bool CanOpen() => true;

    public OpenResults Open()
    {
        try
        {
            if (Playlist.IOStream != null)
            {
                AddPlaylistItem(new()
                {
                    IOStream= Playlist.IOStream,
                    Title   = "Custom IO Stream",
                    FileSize= Playlist.IOStream.Length
                });

                Handler.OnPlaylistCompleted();

                return new();
            }

            // Proper Url Format
            string scheme;
            bool   isWeb    = false;
            string uriType  = "";
            string ext      = Utils.GetUrlExtention(Playlist.Url);
            string localPath= null;

            try
            {
                Uri uri     = new(Playlist.Url);
                scheme      = uri.Scheme.ToLower();
                isWeb       = scheme.StartsWith("http");
                uriType     = uri.IsFile ? "file" : (uri.IsUnc ? "unc" : "");
                localPath   = uri.LocalPath;
            } catch { }

            // Playlists (M3U, M3U8, PLS | TODO: WPL, XSPF)
            if (ext == "m3u")// || ext == "m3u8")
            {
                Playlist.InputType = InputType.Web; // TBR: Can be mixed
                Playlist.FolderBase = Path.GetTempPath();

                var items = isWeb ? M3UPlaylist.ParseFromHttp(Playlist.Url) : M3UPlaylist.Parse(Playlist.Url);

                foreach(var mitem in items)
                {
                    AddPlaylistItem(new()
                    {
                        Title       = mitem.Title,
                        Url         = mitem.Url,
                        DirectUrl   = mitem.Url,
                        UserAgent   = mitem.UserAgent,
                        Referrer    = mitem.Referrer
                    });
                }

                Handler.OnPlaylistCompleted();

                return new();
            }
            else if (ext == "pls")
            {
                Playlist.InputType = InputType.Web; // TBR: Can be mixed
                Playlist.FolderBase = Path.GetTempPath();

                var items = PLSPlaylist.Parse(Playlist.Url);

                foreach(var mitem in items)
                {
                    AddPlaylistItem(new PlaylistItem()
                    {
                        Title       = mitem.Title,
                        Url         = mitem.Url,
                        DirectUrl   = mitem.Url,
                        // Duration
                    });
                }

                Handler.OnPlaylistCompleted();

                return new();
            }

            FileInfo fi = null;
            // Single Playlist Item
            if (uriType == "file")
            {
                Playlist.InputType = InputType.File;
                if (File.Exists(Playlist.Url))
                {
                    fi = new(Playlist.Url);
                    Playlist.FolderBase = fi.DirectoryName;
                }
            }
            else if (isWeb)
            {
                Playlist.InputType = InputType.Web;
                Playlist.FolderBase = Path.GetTempPath();
            }
            else if (uriType == "unc")
            {
                Playlist.InputType = InputType.UNC;
                Playlist.FolderBase = Path.GetTempPath();
            }
            else
            {
                //Playlist.InputType = InputType.Unknown;
                Playlist.FolderBase = Path.GetTempPath();
            }

            PlaylistItem item = new()
            {
                Url         = Playlist.Url,
                DirectUrl   = Playlist.Url
            };

            if (fi == null && File.Exists(Playlist.Url))
            {
                fi              = new(Playlist.Url);
                item.Title      = fi.Name;
                item.FileSize   = fi.Length;
            }
            else
            {
                if (localPath != null)
                    item.Title = Path.GetFileName(localPath);

                if (item.Title == null || item.Title.Trim().Length == 0)
                    item.Title = Playlist.Url;
            }

            AddPlaylistItem(item);
            Handler.OnPlaylistCompleted();

            return new();
        } catch (Exception e)
        {
            return new(e.Message);
        }
    }

    public OpenResults OpenItem() => new();

    public void ScrapeItem(PlaylistItem item)
    {
        // Update Title (TBR: don't mess with other media types - only movies/tv shows)
        if (Playlist.InputType != InputType.File && Playlist.InputType != InputType.UNC && Playlist.InputType != InputType.Torrent)
            return;

        item.FillMediaParts();
    }
}

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaFramework.MediaDecoder;

public abstract unsafe class DecoderBase : RunThreadBase
{
    public MediaType                Type            { get; protected set; }

    public bool                     OnVideoDemuxer  => demuxer?.Type == MediaType.Video;
    public Demuxer                  Demuxer         => demuxer;
    public StreamBase               Stream          { get; protected set; }
    public AVCodecContext*          CodecCtx        => codecCtx;
    public int                      Width           => codecCtx != null ? codecCtx->width : 0;  // for subtitle
    public int                      Height          => codecCtx != null ? codecCtx->height : 0; // for subtitle

    public Action<DecoderBase>      CodecChanged    { get; set; }
    public Config                   Config          { get; protected set; }
    public double                   Speed           { get => speed; set { if (Disposed) { speed = value; return; } if (speed != value) OnSpeedChanged(value); } }
    protected double speed = 1, oldSpeed = 1;
    protected virtual void OnSpeedChanged(double value) { }

    internal bool               filledFromCodec;
    protected AVFrame*          frame;
    protected AVCodecContext*   codecCtx;
    internal  object            lockCodecCtx    = new();

    protected Demuxer           demuxer;

    public DecoderBase(Config config, int uniqueId = -1) : base(uniqueId)
    {
        Config = config;

        if (this is VideoDecoder)
            Type = MediaType.Video;
        else if (this is AudioDecoder)
            Type = MediaType.Audio;
        else if (this is SubtitlesDecoder)
            Type = MediaType.Subs;
        else if (this is DataDecoder)
            Type = MediaType.Data;

        threadName = $"Decoder: {Type,5}";
    }

    public string Open(StreamBase stream)
    {
        lock (lockActions)
        {
            var prevStream = Stream;
            Dispose();
            Status = Status.Opening;
            string error = Open2(stream, prevStream);
            if (!Disposed)
                frame = av_frame_alloc();

            return error;
        }
    }
    protected string Open2(StreamBase stream, StreamBase prevStream, bool openStream = true)
    {
        string error = null;

        try
        {
            lock (stream.Demuxer.lockActions)
            {
                if (stream == null || stream.Demuxer.Interrupter.ForceInterrupt == 1 || stream.Demuxer.Disposed)
                    return "Cancelled";

                int ret = -1;
                Disposed= false;
                Stream  = stream;
                demuxer = stream.Demuxer;

                if (stream is not DataStream) // if we don't open/use a data codec context why not just push the Data Frames directly from the Demuxer? no need to have DataDecoder*
                {
                    // avcodec_find_decoder will use libdav1d which does not support hardware decoding (software fallback with openStream = false from av1 to default:libdav1d) [#340]
                    var codec = stream.CodecID == AVCodecID.Av1 && openStream && Config.Video.VideoAcceleration ? avcodec_find_decoder_by_name("av1") : avcodec_find_decoder(stream.CodecID);
                    if (codec == null)
                        return error = $"[{Type} avcodec_find_decoder] No suitable codec found";

                    codecCtx = avcodec_alloc_context3(codec); // Pass codec to use default settings
                    if (codecCtx == null)
                        return error = $"[{Type} avcodec_alloc_context3] Failed to allocate context3";

                    ret = avcodec_parameters_to_context(codecCtx, stream.AVStream->codecpar);
                    if (ret < 0)
                        return error = $"[{Type} avcodec_parameters_to_context] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})";

                    codecCtx->pkt_timebase  = stream.AVStream->time_base;
                    codecCtx->codec_id      = codec->id; // avcodec_parameters_to_context will change this we need to set Stream's Codec Id (eg we change mp2 to mp3)

                    if (Config.Decoder.ShowCorrupted)
                        codecCtx->flags |= CodecFlags.OutputCorrupt;

                    if (Config.Decoder.LowDelay)
                        codecCtx->flags |= CodecFlags.LowDelay;

                    try { ret = Setup(codec); } catch(Exception e) { return error = $"[{Type} Setup] {e.Message}"; }
                    if (ret < 0)
                        return error = $"[{Type} Setup] {ret}";

                    var codecOpts = Config.Decoder.GetCodecOptPtr(stream.Type);
                    AVDictionary* avopt = null;
                    foreach(var optKV in codecOpts)
                        av_dict_set(&avopt, optKV.Key, optKV.Value, 0);

                    ret = avcodec_open2(codecCtx, null, avopt == null ? null : &avopt);

                    if (avopt != null)
                    {
                        if (ret >= 0)
                        {
                            AVDictionaryEntry *t = null;

                            while ((t = av_dict_get(avopt, "", t, DictReadFlags.IgnoreSuffix)) != null)
                                Log.Debug($"Ignoring codec option {Utils.BytePtrToStringUTF8(t->key)}");
                        }

                        av_dict_free(&avopt);
                    }

                    if (ret < 0)
                        return error = $"[{Type} avcodec_open2] {FFmpegEngine.ErrorCodeToMsg(ret)} ({ret})";
                }

                if (openStream)
                {
                    if (prevStream != null)
                    {
                        if (prevStream.Demuxer.Type == stream.Demuxer.Type)
                            stream.Demuxer.SwitchStream(stream);
                        else if (!prevStream.Demuxer.Disposed)
                        {
                            if (prevStream.Demuxer.Type == MediaType.Video)
                                prevStream.Demuxer.DisableStream(prevStream);
                            else if (prevStream.Demuxer.Type == MediaType.Audio || prevStream.Demuxer.Type == MediaType.Subs)
                                prevStream.Demuxer.Dispose();

                            stream.Demuxer.EnableStream(stream);
                        }
                    }
                    else
                        stream.Demuxer.EnableStream(stream);

                    Status = Status.Stopped;
                    CodecChanged?.Invoke(this);
                }

                return null;
            }
        }
        finally
        {
            if (error != null)
                Dispose(true);
        }
    }
    protected abstract int Setup(AVCodec* codec);

    public void Dispose(bool closeStream = false)
    {
        if (Disposed)
            return;

        lock (lockActions)
        {
            if (Disposed)
                return;

            Stop();
            DisposeInternal();

            if (closeStream && Stream != null && !Stream.Demuxer.Disposed)
            {
                if (Stream.Demuxer.Type == MediaType.Video)
                    Stream.Demuxer.DisableStream(Stream);
                else
                    Stream.Demuxer.Dispose();
            }

            if (frame != null)
                fixed (AVFrame** ptr = &frame)
                    av_frame_free(ptr);

            if (codecCtx != null)
                fixed (AVCodecContext** ptr = &codecCtx)
                    avcodec_free_context(ptr);

            codecCtx        = null;
            demuxer         = null;
            Stream          = null;
            Status          = Status.Stopped;
            Disposed        = true;
            Log.Info("Disposed");
        }
    }
    protected abstract void DisposeInternal();
}

using System.Windows;
using LLPlayer.Views;

namespace LLPlayer.Extensions;

public partial class MyDialogWindow : Window, IDialogWindow
{
    public IDialogResult? Result { get; set; }

    public MyDialogWindow()
    {
        InitializeComponent();

        MainWindow.SetTitleBarDarkMode(this);
    }
}

using System.Threading;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework;

public abstract class RunThreadBase : NotifyPropertyChanged
{
    Status _Status = Status.Stopped;
    public Status               Status          {
        get => _Status;
        set
        {
            lock (lockStatus)
            {
                if (CanDebug && _Status != Status.QueueFull && value != Status.QueueFull && _Status != Status.QueueEmpty && value != Status.QueueEmpty)
                    Log.Debug($"{_Status} -> {value}");

                _Status = value;
            }
        }
    }
    public bool                 IsRunning       {
        get
        {
            bool ret = false;
            lock (lockStatus) ret = thread != null && thread.IsAlive && Status != Status.Paused;
            return ret;
        }
    }

    public bool                 CriticalArea    { get; protected set; }
    public bool                 Disposed        { get; protected set; } = true;
    public int                  UniqueId        { get; protected set; } = -1;
    public bool                 PauseOnQueueFull{ get; set; }

    protected Thread            thread;
    protected AutoResetEvent    threadARE       = new(false);
    protected string            threadName      {
        get => _threadName;
        set
        {
            _threadName = value;
            Log = new LogHandler(("[#" + UniqueId + "]").PadRight(8, ' ') + $" [{threadName}] ");
        }
    }
    string _threadName;

    internal LogHandler         Log;
    internal object             lockActions     = new();
    internal object             lockStatus      = new();

    public RunThreadBase(int uniqueId = -1)
        => UniqueId = uniqueId == -1 ? Utils.GetUniqueId() : uniqueId;

    public void Pause()
    {
        lock (lockActions)
        {
            lock (lockStatus)
            {
                PauseOnQueueFull = false;

                if (Disposed || thread == null || !thread.IsAlive || Status == Status.Stopping || Status == Status.Stopped || Status == Status.Ended || Status == Status.Pausing || Status == Status.Paused) return;
                Status = Status.Pausing;
            }
            while (Status == Status.Pausing) Thread.Sleep(5);
        }
    }
    public void Start()
    {
        lock (lockActions)
        {
            int retries = 1;
            while (thread != null && thread.IsAlive && CriticalArea)
            {
                Thread.Sleep(5); // use small steps to re-check CriticalArea (demuxer can have 0 packets again after processing the received ones)
                retries++;
                if (retries > 16)
                {
                    if (CanTrace) Log.Trace($"Start() exhausted");
                    return;
                }
            }

            lock (lockStatus)
            {
                if (Disposed) return;

                PauseOnQueueFull = false;

                if (Status == Status.Draining) while (Status != Status.Draining) Thread.Sleep(3);
                if (Status == Status.Stopping) while (Status != Status.Stopping) Thread.Sleep(3);
                if (Status == Status.Pausing)  while (Status != Status.Pausing)  Thread.Sleep(3);

                if (Status == Status.Ended) return;

                if (Status == Status.Paused)
                {
                    threadARE.Set();
                    while (Status == Status.Paused) Thread.Sleep(3);
                    return;
                }

                if (thread != null && thread.IsAlive) return; // might re-check CriticalArea

                thread = new Thread(() => Run());
                Status = Status.Running;

                thread.Name = $"[#{UniqueId}] [{threadName}]"; thread.IsBackground= true; thread.Start();
                while (!thread.IsAlive) { if (CanTrace) Log.Trace("Waiting thread to come up"); Thread.Sleep(3); }
            }
        }
    }
    public void Stop()
    {
        lock (lockActions)
        {
            lock (lockStatus)
            {
                PauseOnQueueFull = false;

                if (Disposed || thread == null || !thread.IsAlive || Status == Status.Stopping || Status == Status.Stopped || Status == Status.Ended) return;
                if (Status == Status.Pausing) while (Status != Status.Pausing) Thread.Sleep(3);
                Status = Status.Stopping;
                threadARE.Set();
            }

            while (Status == Status.Stopping && thread != null && thread.IsAlive) Thread.Sleep(5);
        }
    }

    protected void Run()
    {
        if (CanDebug) Log.Debug($"Thread started ({Status})");

        do
        {
            RunInternal();

            if (Status == Status.Pausing)
            {
                threadARE.Reset();
                Status = Status.Paused;
                threadARE.WaitOne();
                if (Status == Status.Paused)
                {
                    if (CanDebug) Log.Debug($"{_Status} -> {Status.Running}");
                    _Status = Status.Running;
                }
            }

        } while (Status == Status.Running);

        if (Status != Status.Ended) Status = Status.Stopped;

        if (CanDebug) Log.Debug($"Thread stopped ({Status})");
    }
    protected abstract void RunInternal();
}

public enum Status
{
    Opening,

    Stopping,
    Stopped,

    Pausing,
    Paused,

    Running,
    QueueFull,
    QueueEmpty,
    Draining,

    Ended
}

using System.Collections.Generic;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaPlayer;

namespace FlyleafLib.MediaFramework.MediaStream;

public abstract unsafe class StreamBase : NotifyPropertyChanged
{
    public ExternalStream               ExternalStream      { get; set; }

    public Demuxer                      Demuxer             { get; internal set; }
    public AVStream*                    AVStream            { get; internal set; }
    internal playlist*                  HLSPlaylist         { get; set; }
    public int                          StreamIndex         { get; internal set; } = -1;
    public double                       Timebase            { get; internal set; }

    // TBR: To update Pop-up menu's (Player.Audio/Player.Video ... should inherit this?)
    public bool                         Enabled
    {
        get => _Enabled;
        internal set
        {
            Utils.UI(() =>
            {
                Set(ref _Enabled, value);

                Raise(nameof(EnabledPrimarySubtitle));
                Raise(nameof(EnabledSecondarySubtitle));
                Raise(nameof(SubtitlesStream.SelectedSubMethods));
            });
        }
    }

    bool _Enabled;

    public long                         BitRate             { get; internal set; }
    public Language                     Language            { get; internal set; }
    public string                       Title               { get; internal set; }
    public string                       Codec               { get; internal set; }

    public AVCodecID                    CodecID             { get; internal set; }
    public long                         StartTime           { get; internal set; }
    public long                         StartTimePts        { get; internal set; }
    public long                         Duration            { get; internal set; }
    public Dictionary<string, string>   Metadata            { get; internal set; } = new Dictionary<string, string>();
    public MediaType                    Type                { get; internal set; }

    #region Subtitles
    // TODO: L: Used for subtitle streams only, but defined in the base class
    public bool EnabledPrimarySubtitle => Enabled && this.GetSubEnabled(0);
    public bool EnabledSecondarySubtitle => Enabled && this.GetSubEnabled(1);
    #endregion

    public abstract string GetDump();
    public StreamBase() { }
    public StreamBase(Demuxer demuxer, AVStream* st)
    {
        Demuxer     = demuxer;
        AVStream    = st;
    }

    public virtual void Refresh()
    {
        BitRate     = AVStream->codecpar->bit_rate;
        CodecID     = AVStream->codecpar->codec_id;
        Codec       = avcodec_get_name(AVStream->codecpar->codec_id);
        StreamIndex = AVStream->index;
        Timebase    = av_q2d(AVStream->time_base) * 10000.0 * 1000.0;
        StartTime   = AVStream->start_time != AV_NOPTS_VALUE && Demuxer.hlsCtx == null ? (long)(AVStream->start_time * Timebase) : Demuxer.StartTime;
        StartTimePts= AVStream->start_time != AV_NOPTS_VALUE ? AVStream->start_time : av_rescale_q(StartTime/10, Engine.FFmpeg.AV_TIMEBASE_Q, AVStream->time_base);
        Duration    = AVStream->duration   != AV_NOPTS_VALUE ? (long)(AVStream->duration * Timebase) : Demuxer.Duration;
        Type        = this is VideoStream ? MediaType.Video : (this is AudioStream ? MediaType.Audio : (this is SubtitlesStream ? MediaType.Subs : MediaType.Data));

        if (Demuxer.hlsCtx != null)
        {
            for (int i=0; i<Demuxer.hlsCtx->n_playlists; i++)
            {
                playlist** playlists = Demuxer.hlsCtx->playlists;
                for (int l=0; l<playlists[i]->n_main_streams; l++)
                    if (playlists[i]->main_streams[l]->index == StreamIndex)
                    {
                        Demuxer.Log.Debug($"Stream #{StreamIndex} Found in playlist {i}");
                        HLSPlaylist = playlists[i];
                        break;
                    }
            }
        }

        Metadata.Clear();

        AVDictionaryEntry* b = null;
        while (true)
        {
            b = av_dict_get(AVStream->metadata, "", b, DictReadFlags.IgnoreSuffix);
            if (b == null) break;
            Metadata.Add(Utils.BytePtrToStringUTF8(b->key), Utils.BytePtrToStringUTF8(b->value));
        }

        foreach (var kv in Metadata)
        {
            string keyLower = kv.Key.ToLower();

            if (Language == null && (keyLower == "language" || keyLower == "lang"))
                Language = Language.Get(kv.Value);
            else if (keyLower == "title")
                Title = kv.Value;
        }

        if (Language == null)
            Language = Language.Unknown;
    }
}

using LLPlayer.ViewModels;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Views;

public partial class SelectLanguageDialog : UserControl
{
    public SelectLanguageDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SelectLanguageDialogVM>();
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        Window? window = Window.GetWindow(this);
        window!.CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, OnFindExecuted));
    }

    private void OnFindExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        SearchBox.Focus();
        SearchBox.SelectAll();
        e.Handled = true;
    }
}

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;

namespace FlyleafLib;

public class Language : IEquatable<Language>
{
    public string       CultureName    { get => _CultureName; set
        {   // Required for XML load
            Culture = CultureInfo.GetCultureInfo(value);
            Refresh(this);
        }
    }
    string _CultureName;

    [JsonIgnore]
    public string       TopEnglishName  { get; private set; }
    [JsonIgnore]
    public string       TopNativeName   { get; private set; }

    [JsonIgnore]
    public CultureInfo  Culture         { get; private set; }

    [JsonIgnore]
    public CultureInfo  TopCulture      { get; private set; }
    [JsonIgnore]
    public string       DisplayName => $"{TopEnglishName} ({TopNativeName})";

    [JsonIgnore]
    public string       ISO6391         { get; private set; }

    [JsonIgnore]
    public string       IdSubLanguage   { get; private set; } // Can search for online subtitles with this id

    [JsonIgnore]
    public string       OriginalInput   { get; private set; } // Only for Undetermined language (return clone)

    [JsonIgnore]
    public bool         IsRTL           { get; private set; }

    public override string ToString() => OriginalInput ?? TopEnglishName;

    public override int GetHashCode() => ToString().GetHashCode();

    public override bool Equals(object obj) => Equals(obj as Language);

    public bool Equals(Language lang)
    {
        if (lang is null)
            return false;

        if (ReferenceEquals(this, lang))
            return true;

        if (lang.Culture == null && Culture == null)
        {
            if (OriginalInput != null || lang.OriginalInput != null)
                return OriginalInput == lang.OriginalInput;

            return true; // und
        }

        return lang.IdSubLanguage == IdSubLanguage; // TBR: top level will be equal with lower
    }

    public static bool operator ==(Language lang1, Language lang2) => lang1 is null ? lang2 is null ? true : false : lang1.Equals(lang2);

    public static bool operator !=(Language lang1, Language lang2) => !(lang1 == lang2);

    private static readonly HashSet<string> RTLCodes =
    [
        "ae",
        "ar",
        "dv",
        "fa",
        "ha",
        "he",
        "ks",
        "ku",
        "ps",
        "sd",
        "ug",
        "ur",
        "yi"
    ];

    public static void Refresh(Language lang)
    {
        lang._CultureName = lang.Culture.Name;

        lang.TopCulture = lang.Culture;
        while (lang.TopCulture.Parent.Name != "")
            lang.TopCulture = lang.TopCulture.Parent;

        lang.TopEnglishName = lang.TopCulture.EnglishName;
        lang.TopNativeName = lang.TopCulture.NativeName;
        lang.IdSubLanguage = lang.Culture.ThreeLetterISOLanguageName;
        lang.ISO6391 = lang.Culture.TwoLetterISOLanguageName;
        lang.IsRTL = RTLCodes.Contains(lang.ISO6391);
    }

    public static Language Get(CultureInfo cult)
    {
        Language lang = new() { Culture = cult };
        Refresh(lang);

        return lang;
    }
    public static Language Get(string name)
    {
        Language lang = new() { Culture = StringToCulture(name) };
        if (lang.Culture != null)
            Refresh(lang);
        else
        {
            lang.IdSubLanguage  = "und";
            lang.TopEnglishName   = "Unknown";
            if (name != "und")
                lang.OriginalInput  = name;
        }

        return lang;
    }

    public static CultureInfo StringToCulture(string lang)
    {
        if (string.IsNullOrWhiteSpace(lang) || lang.Length < 2 || lang == "und")
            return null;

        string langLower = lang.ToLower();
        CultureInfo ret = null;

        try
        {
            ret = lang.Length == 3 ? ThreeLetterToCulture(langLower) : CultureInfo.GetCultureInfo(langLower);
        } catch { }

        StringComparer cmp = StringComparer.OrdinalIgnoreCase;

        // TBR: Check also -Country/region two letters?
        if (ret == null || ret.ThreeLetterISOLanguageName == "")
            foreach (var cult in CultureInfo.GetCultures(CultureTypes.AllCultures))
                if (cmp.Equals(cult.Name, langLower) || cmp.Equals(cult.NativeName, langLower) || cmp.Equals(cult.EnglishName, langLower))
                    return cult;

        return ret;
    }

    public static CultureInfo ThreeLetterToCulture(string lang)
    {
        if (lang == "zht")
            return CultureInfo.GetCultureInfo("zh-Hant");
        else if (lang == "pob")
            return CultureInfo.GetCultureInfo("pt-BR");
        else if (lang == "nor")
            return CultureInfo.GetCultureInfo("nob");
        else if (lang == "scc")
            return CultureInfo.GetCultureInfo("srp");
        else if (lang == "tgl")
            return CultureInfo.GetCultureInfo("fil");

        CultureInfo ret = CultureInfo.GetCultureInfo(lang);

        if (ret.ThreeLetterISOLanguageName == "")
        {
            ISO639_2B_TO_2T.TryGetValue(lang, out string iso639_2t);
            if (iso639_2t != null)
                ret = CultureInfo.GetCultureInfo(iso639_2t);
        }

        return ret.ThreeLetterISOLanguageName == "" ? null : ret;
    }

    public static readonly Dictionary<string, string> ISO639_2T_TO_2B = new()
    {
        { "bod","tib" },
        { "ces","cze" },
        { "cym","wel" },
        { "deu","ger" },
        { "ell","gre" },
        { "eus","baq" },
        { "fas","per" },
        { "fra","fre" },
        { "hye","arm" },
        { "isl","ice" },
        { "kat","geo" },
        { "mkd","mac" },
        { "mri","mao" },
        { "msa","may" },
        { "mya","bur" },
        { "nld","dut" },
        { "ron","rum" },
        { "slk","slo" },
        { "sqi","alb" },
        { "zho","chi" },
    };
    public static readonly Dictionary<string, string> ISO639_2B_TO_2T = new()
    {
        { "alb","sqi" },
        { "arm","hye" },
        { "baq","eus" },
        { "bur","mya" },
        { "chi","zho" },
        { "cze","ces" },
        { "dut","nld" },
        { "fre","fra" },
        { "geo","kat" },
        { "ger","deu" },
        { "gre","ell" },
        { "ice","isl" },
        { "mac","mkd" },
        { "mao","mri" },
        { "may","msa" },
        { "per","fas" },
        { "rum","ron" },
        { "slo","slk" },
        { "tib","bod" },
        { "wel","cym" },
    };

    public static Language English = Get("eng");
    public static Language Unknown = Get("und");

    public static List<Language> AllLanguages
    {
        get
        {
            if (field != null)
            {
                return field;
            }

            var neutralCultures = CultureInfo
                .GetCultures(CultureTypes.NeutralCultures)
                .Where(c => !string.IsNullOrEmpty(c.Name));

            var uniqueCultures = neutralCultures
                .GroupBy(c => c.ThreeLetterISOLanguageName)
                .Select(g => g.First());

            List<Language> languages = new();
            foreach (var culture in uniqueCultures)
            {
                languages.Add(Get(culture));
            }
            languages = languages.OrderBy(l => l.TopEnglishName, StringComparer.InvariantCulture).ToList();

            field = languages;

            return field;
        }
    }
}

using System.Windows;
using LLPlayer.ViewModels;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Views;

public partial class ErrorDialog : UserControl
{
    private ErrorDialogVM VM => (ErrorDialogVM)DataContext;

    public ErrorDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<ErrorDialogVM>();
    }

    private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
    {
        Keyboard.Focus(sender as IInputElement);
    }

    private void ErrorDialog_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Keyboard.Focus(sender as IInputElement);
    }

    // Topmost dialog, so it should be draggable
    private void Window_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (sender is not Window window)
            return;

        if (e.ChangedButton == MouseButton.Left)
        {
            window.DragMove();
        }
    }

    // Make TextBox uncopyable
    private void TextBox_PreviewMouseDown(object sender, ExecutedRoutedEventArgs e)
    {
        if (e.Command == ApplicationCommands.Copy ||
            e.Command == ApplicationCommands.Cut ||
            e.Command == ApplicationCommands.Paste)
        {
            e.Handled = true;

            if (e.Command == ApplicationCommands.Copy)
            {
                // instead trigger copy command
                VM.CmdCopyMessage.Execute();
            }
        }
    }
}

using System.IO;
using System.Linq;
using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaPlayer;

namespace FlyleafLib.MediaFramework.MediaStream;

public enum SelectSubMethod
{
    Original,
    OCR
}

// Both external and internal subtitles implement this interface and use it with PopMenu.
public interface ISubtitlesStream
{
    public bool EnabledPrimarySubtitle { get; }
    public bool EnabledSecondarySubtitle { get; }
}

public class SelectedSubMethod
{
    private readonly ISubtitlesStream _stream;

    public SelectedSubMethod(ISubtitlesStream stream, SelectSubMethod method)
    {
        _stream = stream;
        SelectSubMethod = method;
    }

    public SelectSubMethod SelectSubMethod { get; }

    public bool IsPrimaryEnabled => _stream.EnabledPrimarySubtitle
                                    && SelectSubMethod == SubtitlesSelectedHelper.PrimaryMethod;

    public bool IsSecondaryEnabled => _stream.EnabledSecondarySubtitle
                                    && SelectSubMethod == SubtitlesSelectedHelper.SecondaryMethod;
}

public unsafe class SubtitlesStream : StreamBase, ISubtitlesStream
{
    public bool IsBitmap { get; set; }

    public SelectedSubMethod[] SelectedSubMethods {
        get
        {
            var methods = (SelectSubMethod[])Enum.GetValues(typeof(SelectSubMethod));
            if (!IsBitmap)
            {
                // delete OCR if text sub
                methods = methods.Where(m => m != SelectSubMethod.OCR).ToArray();
            }

            return methods.
                Select(m => new SelectedSubMethod(this, m)).ToArray();
        }
    }

    public string DisplayMember => $"[#{StreamIndex}] {Language} ({(IsBitmap ? "BMP" : "TXT")})";

    public override string GetDump()
        => $"[{Type}  #{StreamIndex}-{Language.IdSubLanguage}{(Title != null ? "(" + Title + ")" : "")}] {Codec} | [BR: {BitRate}] | {Utils.TicksToTime((long)(AVStream->start_time * Timebase))}/{Utils.TicksToTime((long)(AVStream->duration * Timebase))} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}";

    public SubtitlesStream() { }
    public SubtitlesStream(Demuxer demuxer, AVStream* st) : base(demuxer, st) => Refresh();

    public override void Refresh()
    {
        base.Refresh();

        var codecDescr  = avcodec_descriptor_get(CodecID);
        IsBitmap        = codecDescr != null && (codecDescr->props & CodecPropFlags.BitmapSub) != 0;

        if (Demuxer.FormatContext->nb_streams == 1) // External Streams (mainly for .sub will have as start time the first subs timestamp)
            StartTime = 0;
    }

    public void ExternalStreamAdded()
    {
        // VobSub (parse .idx data to extradata - based on .sub url)
        if (CodecID == AVCodecID.DvdSubtitle && ExternalStream != null && ExternalStream.Url.EndsWith(".sub", StringComparison.OrdinalIgnoreCase))
        {
            var idxFile = ExternalStream.Url.Substring(0, ExternalStream.Url.Length - 3) + "idx";
            if (File.Exists(idxFile))
            {
                var bytes = File.ReadAllBytes(idxFile);
                AVStream->codecpar->extradata = (byte*)av_malloc((nuint)bytes.Length);
                AVStream->codecpar->extradata_size = bytes.Length;
                Span<byte> src = new(bytes);
                Span<byte> dst = new(AVStream->codecpar->extradata, bytes.Length);
                src.CopyTo(dst);
            }
        }
    }
}

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.Plugins;

public unsafe class StreamSuggester : PluginBase, ISuggestPlaylistItem, ISuggestAudioStream, ISuggestVideoStream, ISuggestSubtitlesStream, ISuggestSubtitles, ISuggestBestExternalSubtitles
{
    public new int Priority { get; set; } = 3000;

    public AudioStream SuggestAudio(ObservableCollection<AudioStream> streams)
    {
        lock (streams[0].Demuxer.lockActions)
        {
            foreach (var lang in Config.Audio.Languages)
                foreach (var stream in streams)
                    if (stream.Language == lang)
                    {
                        if (stream.Demuxer.Programs.Count < 2)
                        {
                            Log.Info($"Audio based on language");
                            return stream;
                        }

                        for (int i = 0; i < stream.Demuxer.Programs.Count; i++)
                        {
                            bool aExists = false, vExists = false;
                            foreach (var pstream in stream.Demuxer.Programs[i].Streams)
                            {
                                if (pstream.StreamIndex == stream.StreamIndex) aExists = true;
                                else if (pstream.StreamIndex == stream.Demuxer.VideoStream?.StreamIndex) vExists = true;
                            }

                            if (aExists && vExists)
                            {
                                Log.Info($"Audio based on language and same program #{i}");
                                return stream;
                            }
                        }
                    }

            // Fall-back to FFmpeg's default
            int streamIndex;
            lock (streams[0].Demuxer.lockFmtCtx)
                streamIndex = av_find_best_stream(streams[0].Demuxer.FormatContext, AVMediaType.Audio, -1, streams[0].Demuxer.VideoStream != null ? streams[0].Demuxer.VideoStream.StreamIndex : -1, null, 0);

            foreach (var stream in streams)
                if (stream.StreamIndex == streamIndex)
                {
                    Log.Info($"Audio based on av_find_best_stream");
                    return stream;
                }

            if (streams.Count > 0) // FFmpeg will not suggest anything when findstreaminfo is disable
                return streams[0];

            return null;
        }
    }

    public VideoStream SuggestVideo(ObservableCollection<VideoStream> streams)
    {
        // Try to find best video stream based on current screen resolution
        var iresults =
            from vstream in streams
            where vstream.Type == MediaType.Video && vstream.Height <= Config.Video.MaxVerticalResolution //Decoder.VideoDecoder.Renderer.Info.ScreenBounds.Height
            orderby vstream.Height descending
            select vstream;

        List<VideoStream> results = iresults.ToList();

        if (results.Count != 0)
            return iresults.ToList()[0];
        else
        {
            // Fall-back to FFmpeg's default
            int streamIndex;
            lock (streams[0].Demuxer.lockFmtCtx)
                streamIndex = av_find_best_stream(streams[0].Demuxer.FormatContext, AVMediaType.Video, -1, -1, null, 0);
            if (streamIndex < 0) return null;

            foreach (var vstream in streams)
                if (vstream.StreamIndex == streamIndex)
                    return vstream;
        }

        if (streams.Count > 0) // FFmpeg will not suggest anything when findstreaminfo is disable
            return streams[0];

        return null;
    }

    public PlaylistItem SuggestItem()
        => Playlist.Items[0];

    public void SuggestSubtitles(out SubtitlesStream stream, out ExternalSubtitlesStream extStream)
    {
        stream = null;
        extStream = null;

        List<Language> langs = new();

        foreach (var lang in Config.Subtitles.Languages)
            langs.Add(lang);

        langs.Add(Language.Unknown);

        var extStreams = Selected.ExternalSubtitlesStreamsAll
            .Where(x => (Config.Subtitles.OpenAutomaticSubs || !x.Automatic))
            .OrderBy(x => x.Language.ToString())
            .ThenBy(x => x.Downloaded)
            .ThenBy(x => x.ManualDownloaded);

        foreach (var lang in langs)
        {
            foreach(var embStream in decoder.VideoDemuxer.SubtitlesStreamsAll)
                if (embStream.Language == lang)
                {
                    stream = embStream;
                    return;
                }

            foreach(var extStream2 in extStreams)
                if (extStream2.Language == lang)
                {
                    extStream = extStream2;
                    return;
                }
        }
    }

    public ExternalSubtitlesStream SuggestBestExternalSubtitles()
    {
        var extStreams = Selected.ExternalSubtitlesStreamsAll
            .Where(x => (Config.Subtitles.OpenAutomaticSubs || !x.Automatic))
            .OrderBy(x => x.Language.ToString())
            .ThenBy(x => x.Downloaded)
            .ThenBy(x => x.ManualDownloaded);

        foreach(var extStream in extStreams)
            if (extStream.Language == Config.Subtitles.Languages[0])
                return extStream;

        return null;
    }

    public SubtitlesStream SuggestSubtitles(ObservableCollection<SubtitlesStream> streams, List<Language> langs)
    {
        foreach(var lang in langs)
            foreach(var stream in streams)
                if (lang == stream.Language)
                    return stream;

        return null;
    }
}

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace FlyleafLib.MediaPlayer.Translation.Services;

#nullable enable

public class GoogleV1TranslateService : ITranslateService
{
    internal static Dictionary<string, string> DefaultRegions { get; } = new()
    {
        ["zh"] = "zh-CN",
        ["pt"] = "pt-PT",
        ["fr"] = "fr-FR",
    };

    private readonly HttpClient _httpClient;
    private string? _srcLang;
    private string? _targetLang;
    private readonly GoogleV1TranslateSettings _settings;

    public GoogleV1TranslateService(GoogleV1TranslateSettings settings)
    {
        if (string.IsNullOrWhiteSpace(settings.Endpoint))
        {
            throw new TranslationConfigException(
                $"Endpoint for {ServiceType} is not configured.");
        }

        _settings = settings;
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(settings.Endpoint);
        _httpClient.Timeout = TimeSpan.FromMilliseconds(settings.TimeoutMs);
    }

    public TranslateServiceType ServiceType => TranslateServiceType.GoogleV1;

    public void Dispose()
    {
        _httpClient.Dispose();
    }

    public void Initialize(Language src, TargetLanguage target)
    {
        (TranslateLanguage srcLang, _) = this.TryGetLanguage(src, target);

        _srcLang = ToSourceCode(srcLang.ISO6391);
        _targetLang = ToTargetCode(target);
    }

    private string ToSourceCode(string iso6391)
    {
        if (iso6391 == "nb")
        {
            // handle 'Norwegian Bokmål' as 'Norwegian'
            return "no";
        }

        // ref: https://cloud.google.com/translate/docs/languages?hl=en
        if (!DefaultRegions.TryGetValue(iso6391, out string? defaultRegion))
        {
            // no region languages
            return iso6391;
        }

        // has region languages
        return _settings.Regions.GetValueOrDefault(iso6391, defaultRegion);
    }

    private static string ToTargetCode(TargetLanguage target)
    {
        return target switch
        {
            TargetLanguage.ChineseSimplified => "zh-CN",
            TargetLanguage.ChineseTraditional => "zh-TW",
            TargetLanguage.French => "fr-FR",
            TargetLanguage.FrenchCanadian => "fr-CA",
            TargetLanguage.Portuguese => "pt-PT",
            TargetLanguage.PortugueseBrazilian => "pt-BR",
            _ => target.ToISO6391()
        };
    }

    public async Task<string> TranslateAsync(string text, CancellationToken token)
    {
        string jsonResultString = "";
        int statusCode = -1;

        try
        {
            var url = $"/translate_a/single?client=gtx&sl={_srcLang}&tl={_targetLang}&dt=t&q={Uri.EscapeDataString(text)}";

            using var result = await _httpClient.GetAsync(url, token).ConfigureAwait(false);
            jsonResultString = await result.Content.ReadAsStringAsync(token).ConfigureAwait(false);

            statusCode = (int)result.StatusCode;
            result.EnsureSuccessStatusCode();

            List<string> resultTexts = new();
            using JsonDocument doc = JsonDocument.Parse(jsonResultString);
            resultTexts.AddRange(doc.RootElement[0].EnumerateArray().Select(arr => arr[0].GetString()!.Trim()));

            return string.Join(Environment.NewLine, resultTexts);
        }
        // Distinguish between timeout and cancel errors
        catch (OperationCanceledException ex)
            when (!ex.Message.StartsWith("The request was canceled due to the configured HttpClient.Timeout"))
        {
            // cancel
            throw;
        }
        catch (Exception ex)
        {
            // timeout and other error
            throw new TranslationException($"Cannot request to {ServiceType}: {ex.Message}", ex)
            {
                Data =
                {
                    ["status_code"] = statusCode.ToString(),
                    ["response"] = jsonResultString
                }
            };
        }
    }
}

using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Extensions;

public static class TextBoxMiscHelper
{
    public static bool GetIsHexValidationEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsHexValidationEnabledProperty);
    }

    public static void SetIsHexValidationEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsHexValidationEnabledProperty, value);
    }

    public static readonly DependencyProperty IsHexValidationEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsHexValidationEnabled",
            typeof(bool),
            typeof(TextBoxMiscHelper),
            new UIPropertyMetadata(false, OnIsHexValidationEnabledChanged));

    private static void OnIsHexValidationEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBox textBox)
        {
            if ((bool)e.NewValue)
            {
                textBox.PreviewTextInput += OnPreviewTextInput;
            }
            else
            {
                textBox.PreviewTextInput -= OnPreviewTextInput;
            }
        }
    }

    private static void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        e.Handled = !Regex.IsMatch(e.Text, "^[0-9a-f]+$", RegexOptions.IgnoreCase);
    }
}

using System.Windows;
using FlyleafLib.MediaPlayer;
using LLPlayer.Views;

namespace LLPlayer.Services;

public static class ErrorDialogHelper
{
    public static void ShowKnownErrorPopup(string message, string errorType)
    {
        var dialogService = ((App)Application.Current).Container.Resolve<DialogService>();

        DialogParameters p = new()
        {
            { "type", "known" },
            { "message", message },
            { "errorType", errorType }
        };

        dialogService.ShowDialog(nameof(ErrorDialog), p);
    }

    public static void ShowKnownErrorPopup(string message, KnownErrorType errorType)
    {
        ShowKnownErrorPopup(message, errorType.ToString());
    }

    public static void ShowUnknownErrorPopup(string message, string errorType, Exception? ex = null)
    {
        var dialogService = ((App)Application.Current).Container.Resolve<DialogService>();

        DialogParameters p = new()
        {
            { "type", "unknown" },
            { "message", message },
            { "errorType", errorType },
        };

        if (ex != null)
        {
            p.Add("exception", ex);
        }

        dialogService.ShowDialog(nameof(ErrorDialog), p);
    }

    public static void ShowUnknownErrorPopup(string message, UnknownErrorType errorType, Exception? ex = null)
    {
        ShowUnknownErrorPopup(message, errorType.ToString(), ex);
    }
}

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Data;

using SharpGen.Runtime;
using SharpGen.Runtime.Win32;

using Vortice.MediaFoundation;
using static Vortice.XAudio2.XAudio2;

using FlyleafLib.MediaFramework.MediaDevice;

namespace FlyleafLib;

public class AudioEngine : CallbackBase, IMMNotificationClient, INotifyPropertyChanged
{
    #region Properties (Public)

    public AudioEndpoint DefaultDevice      { get; private set; } = new() { Id = "0", Name = "Default" };
    public AudioEndpoint CurrentDevice      { get; private set; } = new();

    /// <summary>
    /// Whether no audio devices were found or audio failed to initialize
    /// </summary>
    public bool         Failed              { get; private set; }

    /// <summary>
    /// List of Audio Capture Devices
    /// </summary>
    public ObservableCollection<AudioDevice>
                        CapDevices          { get; set; } = new();

    public void         RefreshCapDevices() => AudioDevice.RefreshDevices();

    /// <summary>
    /// List of Audio Devices
    /// </summary>
    public ObservableCollection<AudioEndpoint>
                        Devices             { get; private set; } = new();

    private readonly object lockDevices = new();
    #endregion

    IMMDeviceEnumerator deviceEnum;
    private object      locker = new();

    public event PropertyChangedEventHandler PropertyChanged;

    public AudioEngine()
    {
        if (Engine.Config.DisableAudio)
        {
            Failed = true;
            return;
        }

        BindingOperations.EnableCollectionSynchronization(Devices, lockDevices);
        EnumerateDevices();
    }

    private void EnumerateDevices()
    {
        try
        {
            deviceEnum = new IMMDeviceEnumerator();

            var defaultDevice = deviceEnum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
            if (defaultDevice == null)
            {
                Failed = true;
                return;
            }

            lock (lockDevices)
            {
                Devices.Clear();
                Devices.Add(DefaultDevice);
                foreach (var device in deviceEnum.EnumAudioEndpoints(DataFlow.Render, DeviceStates.Active))
                    Devices.Add(new() { Id = device.Id, Name = device.FriendlyName });
            }

            CurrentDevice.Id    = defaultDevice.Id;
            CurrentDevice.Name  = defaultDevice.FriendlyName;

            if (Logger.CanInfo)
            {
                string dump = "";
                foreach (var device in deviceEnum.EnumAudioEndpoints(DataFlow.Render, DeviceStates.Active))
                    dump += $"{device.Id} | {device.FriendlyName} {(defaultDevice.Id == device.Id ? "*" : "")}\r\n";
                Engine.Log.Info($"Audio Devices\r\n{dump}");
            }

            var xaudio2 = XAudio2Create();

            if (xaudio2 == null)
                Failed = true;
            else
                xaudio2.Dispose();

            deviceEnum.RegisterEndpointNotificationCallback(this);

        } catch { Failed = true; }
    }
    private void RefreshDevices()
    {
        lock (locker)
        {
            Utils.UIInvokeIfRequired(() => // UI Required?
            {
                List<AudioEndpoint> curs     = new();
                List<AudioEndpoint> removed  = new();

                lock (lockDevices)
                {
                    foreach(var device in deviceEnum.EnumAudioEndpoints(DataFlow.Render, DeviceStates.Active))
                        curs.Add(new () { Id = device.Id, Name = device.FriendlyName });

                    foreach(var cur in curs)
                    {
                        bool exists = false;
                        foreach (var device in Devices)
                            if (cur.Id == device.Id)
                                { exists = true; break; }

                        if (!exists)
                        {
                            Engine.Log.Info($"Audio device {cur} added");
                            Devices.Add(cur);
                        }
                    }

                    foreach (var device in Devices)
                    {
                        if (device.Id == "0") // Default
                            continue;

                        bool exists = false;
                        foreach(var cur in curs)
                            if (cur.Id == device.Id)
                                { exists = true; break; }

                        if (!exists)
                        {
                            Engine.Log.Info($"Audio device {device} removed");
                            removed.Add(device);
                        }
                    }

                    foreach(var device in removed)
                        Devices.Remove(device);
                }

                var defaultDevice =  deviceEnum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
                if (defaultDevice != null && CurrentDevice.Id != defaultDevice.Id)
                {
                    CurrentDevice.Id    = defaultDevice.Id;
                    CurrentDevice.Name  = defaultDevice.FriendlyName;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentDevice)));
                }

                // Fall back to DefaultDevice *Non-UI thread otherwise will freeze (not sure where and why) during xaudio.Dispose()
                if (removed.Count > 0)
                    Task.Run(() =>
                    {
                        foreach(var device in removed)
                        {
                            foreach(var player in Engine.Players)
                                if (player.Audio.Device == device)
                                    player.Audio.Device = DefaultDevice;
                        }
                    });
            });
        }
    }

    public void OnDeviceStateChanged(string pwstrDeviceId, int newState) => RefreshDevices();
    public void OnDeviceAdded(string pwstrDeviceId) => RefreshDevices();
    public void OnDeviceRemoved(string pwstrDeviceId) => RefreshDevices();
    public void OnDefaultDeviceChanged(DataFlow flow, Role role, string pwstrDefaultDeviceId) => RefreshDevices();
    public void OnPropertyValueChanged(string pwstrDeviceId, PropertyKey key) { }

    public class AudioEndpoint
    {
        public string Id    { get; set; }
        public string Name  { get; set; }

        public override string ToString()
            => Name;
    }
}

using System.Text;

namespace FlyleafLib;

public static class SubtitleTextUtil
{
    /// <summary>
    /// Flattens the text into a single line.
    /// - If every line (excluding empty lines) starts with dash, returns the original string.
    /// - For all other text, replaces newlines with spaces and flattens into a single line.
    /// </summary>
    public static string FlattenText(string text)
    {
        ArgumentNullException.ThrowIfNull(text);

        ReadOnlySpan<char> span = text.AsSpan();

        // If there are no newlines, return the text as-is
        if (!span.ContainsAny('\r', '\n'))
        {
            return text;
        }

        int length = span.Length;

        // Determine if the first character is dash to enter list mode
        bool startDash = span.Length > 0 && IsDash(span[0]);

        if (startDash)
        {
            char dashChar = span[0];

            // Check if all lines start with dash (ignore empty lines)
            bool allDash = true;
            bool atLineStart = true;
            int i;
            for (i = 0; i < length; i++)
            {
                if (atLineStart)
                {
                    // Skip empty lines
                    if (span[i] == '\r' || span[i] == '\n')
                    {
                        continue;
                    }
                    if (span[i] != dashChar)
                    {
                        allDash = false;
                        // Done checking
                        break;
                    }
                    atLineStart = false;
                }
                else
                {
                    if (span[i] == '\r')
                    {
                        if (i + 1 < length && span[i + 1] == '\n') i++;
                        atLineStart = true;
                    }
                    else if (span[i] == '\n')
                    {
                        atLineStart = true;
                    }
                }
            }

            // If every line starts with dash, return original text
            if (allDash)
            {
                return text;
            }

            // list mode
            StringBuilder sb = new(length);
            bool firstItem = true;
            i = 0;

            while (i < length)
            {
                int start;

                // Start of a dash line
                if (span[i] == dashChar)
                {
                    if (!firstItem)
                    {
                        sb.Append('\n');
                    }
                    // Append until end of line
                    start = i;
                    while (i < length && span[i] != '\r' && span[i] != '\n') i++;
                    sb.Append(span.Slice(start, i - start));
                    firstItem = false;
                    continue;
                }

                // Skip empty lines
                if (span[i] == '\r' || span[i] == '\n')
                {
                    i++;
                    continue;
                }

                // Continuation line
                start = i;
                while (i < length && span[i] != '\r' && span[i] != '\n') i++;
                sb.Append(' ');
                sb.Append(span.Slice(start, i - start));
            }
            return sb.ToString();
        }

        // Default mode: replace all newlines with spaces in one pass
        char[] buffer = new char[length];
        int pos = 0;
        bool lastWasNewline = false;
        for (int i = 0; i < length; i++)
        {
            char c = span[i];
            if (c == '\r' || c == '\n')
            {
                if (!lastWasNewline)
                {
                    buffer[pos++] = ' ';
                    lastWasNewline = true;
                }
            }
            else
            {
                buffer[pos++] = c;
                lastWasNewline = false;
            }
        }
        return new string(buffer, 0, pos);
    }

    private static bool IsDash(char c)
    {
        switch (c)
        {
            case '-':
            case '\u2043': // ⁃ Hyphen bullet
            case '\u2010': // ‐ Hyphen
            case '\u2012': // ‒ Figure dash
            case '\u2013': // – En dash
            case '\u2014': // — Em dash
            case '\u2015': // ― Horizontal bar
            case '\u2212': // − Minus Sign
                return true;
        }

        return false;
    }
}

using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;

namespace LLPlayer.Extensions;

/// <summary>
/// Behavior to add the ability to swap rows in a DataGrid by drag and drop
/// </summary>
public class DataGridRowOrderBehavior<T> : Behavior<DataGrid>
    where T : class
{
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register(nameof(ItemsSource), typeof(IList), typeof (DataGridRowOrderBehavior<T>), new PropertyMetadata(null));

    /// <summary>
    /// ObservableCollection to be reordered bound to DataGrid
    /// </summary>
    public ObservableCollection<T> ItemsSource
    {
        get => (ObservableCollection<T>)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public static readonly DependencyProperty DragTargetNameProperty =
        DependencyProperty.Register(nameof(DragTargetName), typeof(string), typeof (DataGridRowOrderBehavior<T>), new PropertyMetadata(null));

    /// <summary>
    /// Control name of the element to be dragged
    /// </summary>
    public string DragTargetName
    {
        get => (string)GetValue(DragTargetNameProperty);
        set => SetValue(DragTargetNameProperty, value);
    }

    private T? _draggedItem; // Item in row to be dragged

    protected override void OnAttached()
    {
        base.OnAttached();

        if (AssociatedObject == null)
        {
            return;
        }

        AssociatedObject.AllowDrop = true;

        AssociatedObject.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
        AssociatedObject.DragOver += OnDragOver;
        AssociatedObject.Drop += OnDrop;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (AssociatedObject == null)
        {
            return;
        }

        AssociatedObject.AllowDrop = false;

        AssociatedObject.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
        AssociatedObject.DragOver -= OnDragOver;
        AssociatedObject.Drop -= OnDrop;
    }

    private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (ItemsSource == null)
        {
            return;
        }

        // 1: Determines if it is on a "drag handle".
        if (!IsDragHandle(e.OriginalSource))
        {
            return;
        }

        // 2: Identify Row
        DataGridRow? row = UIHelper.FindParent<DataGridRow>((DependencyObject)e.OriginalSource);
        if (row == null)
        {
            return;
        }

        // Select the row
        AssociatedObject.UnselectAll();
        row.IsSelected = true;
        row.Focus();

        _draggedItem = row.Item as T;

        if (_draggedItem != null)
        {
            // Start dragging
            DragDrop.DoDragDrop(AssociatedObject, _draggedItem, DragDropEffects.Move);
        }
    }

    /// <summary>
    /// Dragging (while the mouse is moving)
    /// </summary>
    private void OnDragOver(object sender, DragEventArgs e)
    {
        if (_draggedItem == null)
        {
            return;
        }

        e.Handled = true;

        // Find the line where the mouse is now.
        DataGridRow? row = UIHelper.FindParent<DataGridRow>((DependencyObject)e.OriginalSource);
        if (row == null)
        {
            return;
        }

        var targetItem = row.Item;
        if (targetItem == null || targetItem == _draggedItem)
        {
            // If it's the same line, nothing.
            return;
        }

        T? targetRow = targetItem as T;
        if (targetRow == null)
        {
            return;
        }

        // Get the index of each row to be replaced
        int oldIndex = ItemsSource.IndexOf(_draggedItem);
        int newIndex = ItemsSource.IndexOf(targetRow);

        if (oldIndex < 0 || newIndex < 0 || oldIndex == newIndex)
        {
            return;
        }

        // Swap lines
        ItemsSource.Move(oldIndex, newIndex);
    }

    /// <summary>
    /// When dropped
    /// </summary>
    private void OnDrop(object sender, DragEventArgs e)
    {
        // Clear state as it is being reordered during drag.
        _draggedItem = null;
    }

    /// <summary>
    /// Judges whether an element is ready to start dragging.
    /// </summary>
    private bool IsDragHandle(object originalSource)
    {
        return UIHelper.FindParentWithName(originalSource as DependencyObject, DragTargetName);
    }
}

using System.Collections.Generic;
using System.Linq;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaStream;

namespace FlyleafLib.MediaFramework.MediaProgram;

public class Program
{
    public int      ProgramNumber   { get; internal set; }

    public int      ProgramId       { get; internal set; }

    public IReadOnlyDictionary<string, string>
                    Metadata        { get; internal set; }

    public IReadOnlyList<StreamBase>
                    Streams         { get; internal set; }

    public string   Name            => Metadata.ContainsKey("name") ? Metadata["name"] : string.Empty;

    public unsafe Program(AVProgram* program, Demuxer demuxer)
    {
        ProgramNumber = program->program_num;
        ProgramId = program->id;

        // Load stream info
        List<StreamBase> streams = new(3);
        for(int s = 0; s<program->nb_stream_indexes; s++)
        {
            uint streamIndex = program->stream_index[s];
            StreamBase stream = null;
            stream =  demuxer.AudioStreams.FirstOrDefault(it=>it.StreamIndex == streamIndex);

            if (stream == null)
            {
                stream = demuxer.VideoStreams.FirstOrDefault(it => it.StreamIndex == streamIndex);
                stream ??= demuxer.SubtitlesStreamsAll.FirstOrDefault(it => it.StreamIndex == streamIndex);
            }
            if (stream!=null)
            {
                streams.Add(stream);
            }
        }
        Streams = streams;

        // Load metadata
        Dictionary<string, string> metadata = new();
        AVDictionaryEntry* b = null;
        while (true)
        {
            b = av_dict_get(program->metadata, "", b, DictReadFlags.IgnoreSuffix);
            if (b == null) break;
            metadata.Add(Utils.BytePtrToStringUTF8(b->key), Utils.BytePtrToStringUTF8(b->value));
        }
        Metadata = metadata;
    }
}

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

using FlyleafLib.Plugins;

namespace FlyleafLib;

public class PluginsEngine
{
    public Dictionary<string, PluginType>
                    Types       { get; private set; } = new Dictionary<string, PluginType>();

    public string   Folder      { get; private set; }

    private Type pluginBaseType = typeof(PluginBase);

    internal PluginsEngine()
    {
        Folder = string.IsNullOrEmpty(Engine.Config.PluginsPath) ? null : Utils.GetFolderPath(Engine.Config.PluginsPath);

        LoadAssemblies();
    }

    internal void LoadAssemblies()
    {
        // Load FlyleafLib's Embedded Plugins
        LoadPlugin(Assembly.GetExecutingAssembly());

        // Load External Plugins Folder
        if (Folder != null && Directory.Exists(Folder))
        {
            string[] dirs = Directory.GetDirectories(Folder);

            foreach(string dir in dirs)
                foreach(string file in Directory.GetFiles(dir, "*.dll"))
                    LoadPlugin(Assembly.LoadFrom(Path.GetFullPath(file)));
        }
        else
        {
            Engine.Log.Info($"[PluginHandler] No external plugins found");
        }
    }

    /// <summary>
    /// Manually load plugins
    /// </summary>
    /// <param name="assembly">The assembly to search for plugins</param>
    public void LoadPlugin(Assembly assembly)
    {
        try
        {
            var types = assembly.GetTypes();

            foreach (var type in types)
            {
                if (pluginBaseType.IsAssignableFrom(type) && type.IsClass && !type.IsAbstract)
                {
                    // Force static constructors to execute (For early load, will be useful with c# 8.0 and static properties for interfaces eg. DefaultOptions)
                    // System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type.TypeHandle);

                    if (!Types.ContainsKey(type.Name))
                    {
                        Types.Add(type.Name, new PluginType() { Name = type.Name, Type = type, Version = assembly.GetName().Version});
                        Engine.Log.Info($"Plugin loaded ({type.Name} - {assembly.GetName().Version})");
                    }
                    else
                        Engine.Log.Info($"Plugin already exists ({type.Name} - {assembly.GetName().Version})");
                }
            }
        }
        catch (Exception e) { Engine.Log.Error($"[PluginHandler] [Error] Failed to load assembly ({e.Message} {Utils.GetRecInnerException(e)})"); }
    }
}

using System.Net.Http;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace FlyleafLib.MediaPlayer.Translation.Services;

#nullable enable

public class DeepLXTranslateService : ITranslateService
{
    private readonly HttpClient _httpClient;
    private string? _srcLang;
    private string? _targetLang;
    private readonly DeepLXTranslateSettings _settings;

    public DeepLXTranslateService(DeepLXTranslateSettings settings)
    {
        if (string.IsNullOrWhiteSpace(settings.Endpoint))
        {
            throw new TranslationConfigException(
                $"Endpoint for {ServiceType} is not configured.");
        }

        _settings = settings;
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(settings.Endpoint);
        _httpClient.Timeout = TimeSpan.FromMilliseconds(settings.TimeoutMs);
    }

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    };

    public TranslateServiceType ServiceType => TranslateServiceType.DeepLX;

    public void Dispose()
    {
        _httpClient.Dispose();
    }

    public void Initialize(Language src, TargetLanguage target)
    {
        (TranslateLanguage srcLang, _) = this.TryGetLanguage(src, target);

        _srcLang = ToSourceCode(srcLang.ISO6391);
        _targetLang = ToTargetCode(target);
    }

    private string ToSourceCode(string iso6391)
    {
        return DeepLTranslateService.ToSourceCode(iso6391);
    }

    private string ToTargetCode(TargetLanguage target)
    {
        return DeepLTranslateService.ToTargetCode(target);
    }

    public async Task<string> TranslateAsync(string text, CancellationToken token)
    {
        if (_srcLang == null || _targetLang == null)
        {
            throw new InvalidOperationException("must be initialized");
        }

        string jsonResultString = "";
        int statusCode = -1;

        try
        {
            DeepLXTranslateRequest requestBody = new()
            {
                source_lang = _srcLang,
                target_lang = _targetLang,
                text = text
            };

            string jsonRequest = JsonSerializer.Serialize(requestBody, JsonOptions);
            using StringContent content = new(jsonRequest, Encoding.UTF8, "application/json");

            using var result = await _httpClient.PostAsync("/translate", content, token).ConfigureAwait(false);
            jsonResultString = await result.Content.ReadAsStringAsync(token).ConfigureAwait(false);

            statusCode = (int)result.StatusCode;
            result.EnsureSuccessStatusCode();

            DeepLXTranslateResult? responseData = JsonSerializer.Deserialize<DeepLXTranslateResult>(jsonResultString);
            return responseData!.data;
        }
        catch (OperationCanceledException ex)
            when (!ex.Message.StartsWith("The request was canceled due to the configured HttpClient.Timeout"))
        {
            throw;
        }
        catch (Exception ex)
        {
            throw new TranslationException($"Cannot request to {ServiceType}: {ex.Message}", ex)
            {
                Data =
                {
                    ["status_code"] = statusCode.ToString(),
                    ["response"] = jsonResultString
                }
            };
        }
    }

    private class DeepLXTranslateRequest
    {
        public required string text { get; init; }
        public string? source_lang { get; init; }
        public required string target_lang { get; init; }
    }

    private class DeepLXTranslateResult
    {
        public required string[] alternatives { get; init; }
        public required string data { get; init; }
        public required string source_lang { get; init; }
        public required string target_lang { get; init; }
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.Controls.Settings;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class SettingsDialog : UserControl
{
    public SettingsDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SettingsDialogVM>();
    }

    private void SettingsTreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (SettingsContent == null)
        {
            return;
        }

        if (SettingsTreeView.SelectedItem is TreeViewItem selectedItem)
        {
            string? tag = selectedItem.Tag as string;
            switch (tag)
            {
                case nameof(SettingsPlayer):
                    SettingsContent.Content = new SettingsPlayer();
                    break;

                case nameof(SettingsAudio):
                    SettingsContent.Content = new SettingsAudio();
                    break;

                case nameof(SettingsVideo):
                    SettingsContent.Content = new SettingsVideo();
                    break;

                case nameof(SettingsSubtitles):
                    SettingsContent.Content = new SettingsSubtitles();
                    break;

                case nameof(SettingsSubtitlesPS):
                    SettingsContent.Content = new SettingsSubtitlesPS();
                    break;

                case nameof(SettingsSubtitlesASR):
                    SettingsContent.Content = new SettingsSubtitlesASR();
                    break;

                case nameof(SettingsSubtitlesOCR):
                    SettingsContent.Content = new SettingsSubtitlesOCR();
                    break;

                case nameof(SettingsSubtitlesTrans):
                    SettingsContent.Content = new SettingsSubtitlesTrans();
                    break;

                case nameof(SettingsSubtitlesAction):
                    SettingsContent.Content = new SettingsSubtitlesAction();
                    break;

                case nameof(SettingsKeys):
                    SettingsContent.Content = new SettingsKeys();
                    break;

                case nameof(SettingsKeysOffset):
                    SettingsContent.Content = new SettingsKeysOffset();
                    break;

                case nameof(SettingsMouse):
                    SettingsContent.Content = new SettingsMouse();
                    break;

                case nameof(SettingsThemes):
                    SettingsContent.Content = new SettingsThemes();
                    break;

                case nameof(SettingsPlugins):
                    SettingsContent.Content = new SettingsPlugins();
                    break;

                case nameof(SettingsAbout):
                    SettingsContent.Content = new SettingsAbout();
                    break;
            }
        }
    }
}

using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace FlyleafLib;

#nullable enable

public class TesseractModel : NotifyPropertyChanged, IEquatable<TesseractModel>
{
    public static Dictionary<TesseractOCR.Enums.Language, string> TesseractLangToISO6391 { get; } = new()
    {
        {TesseractOCR.Enums.Language.Afrikaans, "af"},
        {TesseractOCR.Enums.Language.Amharic, "am"},
        {TesseractOCR.Enums.Language.Arabic, "ar"},
        {TesseractOCR.Enums.Language.Assamese, "as"},
        {TesseractOCR.Enums.Language.Azerbaijani, "az"},
        //{TesseractOCR.Enums.Language.AzerbaijaniCyrilic, ""},
        {TesseractOCR.Enums.Language.Belarusian, "be"},
        {TesseractOCR.Enums.Language.Bengali, "bn"},
        {TesseractOCR.Enums.Language.Tibetan, "bo"},
        {TesseractOCR.Enums.Language.Bosnian, "bs"},
        {TesseractOCR.Enums.Language.Breton, "br"},
        {TesseractOCR.Enums.Language.Bulgarian, "bg"},
        {TesseractOCR.Enums.Language.CatalanValencian, "ca"},
        //{TesseractOCR.Enums.Language.Cebuano, ""},
        {TesseractOCR.Enums.Language.Czech, "cs"},
        {TesseractOCR.Enums.Language.ChineseSimplified, "zh"}, // do special handling
        {TesseractOCR.Enums.Language.ChineseTraditional, "zh"},
        //{TesseractOCR.Enums.Language.Cherokee, ""},
        {TesseractOCR.Enums.Language.Corsican, "co"},
        {TesseractOCR.Enums.Language.Welsh, "cy"},
        {TesseractOCR.Enums.Language.Danish, "da"},
        //{TesseractOCR.Enums.Language.DanishFraktur, ""},
        {TesseractOCR.Enums.Language.German, "de"},
        //{TesseractOCR.Enums.Language.GermanFrakturContrib, ""},
        {TesseractOCR.Enums.Language.Dzongkha, "dz"},
        {TesseractOCR.Enums.Language.GreekModern, "el"},
        {TesseractOCR.Enums.Language.English, "en"},
        //{TesseractOCR.Enums.Language.EnglishMiddle, ""},
        {TesseractOCR.Enums.Language.Esperanto, "eo"},
        //{TesseractOCR.Enums.Language.Math, "zz"},
        {TesseractOCR.Enums.Language.Estonian, "et"},
        {TesseractOCR.Enums.Language.Basque, "eu"},
        {TesseractOCR.Enums.Language.Faroese, "fo"},
        {TesseractOCR.Enums.Language.Persian, "fa"},
        //{TesseractOCR.Enums.Language.Filipino, ""},
        {TesseractOCR.Enums.Language.Finnish, "fi"},
        {TesseractOCR.Enums.Language.French, "fr"},
        //{TesseractOCR.Enums.Language.GermanFraktur, ""},
        {TesseractOCR.Enums.Language.FrenchMiddle, "zz"},
        //{TesseractOCR.Enums.Language.WesternFrisian, ""},
        {TesseractOCR.Enums.Language.ScottishGaelic, "gd"},
        {TesseractOCR.Enums.Language.Irish, "ga"},
        {TesseractOCR.Enums.Language.Galician, "gl"},
        //{TesseractOCR.Enums.Language.GreekAncientContrib, ""},
        {TesseractOCR.Enums.Language.Gujarati, "gu"},
        {TesseractOCR.Enums.Language.Haitian, "ht"},
        {TesseractOCR.Enums.Language.Hebrew, "he"},
        {TesseractOCR.Enums.Language.Hindi, "hi"},
        {TesseractOCR.Enums.Language.Croatian, "hr"},
        {TesseractOCR.Enums.Language.Hungarian, "hu"},
        {TesseractOCR.Enums.Language.Armenian, "hy"},
        {TesseractOCR.Enums.Language.Inuktitut, "iu"},
        {TesseractOCR.Enums.Language.Indonesian, "id"},
        {TesseractOCR.Enums.Language.Icelandic, "is"},
        {TesseractOCR.Enums.Language.Italian, "it"},
        //{TesseractOCR.Enums.Language.ItalianOld, ""},
        {TesseractOCR.Enums.Language.Javanese, "jv"},
        {TesseractOCR.Enums.Language.Japanese, "ja"},
        //{TesseractOCR.Enums.Language.JapaneseVertical, ""},
        {TesseractOCR.Enums.Language.Kannada, "kn"},
        {TesseractOCR.Enums.Language.Georgian, "ka"},
        //{TesseractOCR.Enums.Language.GeorgianOld, ""},
        {TesseractOCR.Enums.Language.Kazakh, "kk"},
        {TesseractOCR.Enums.Language.CentralKhmer, "km"},
        {TesseractOCR.Enums.Language.KirghizKyrgyz, "ky"},
        {TesseractOCR.Enums.Language.Kurmanji, "ku"},
        {TesseractOCR.Enums.Language.Korean, "ko"},
        //{TesseractOCR.Enums.Language.KoreanVertical, ""},
        //{TesseractOCR.Enums.Language.KurdishArabicScript, ""},
        {TesseractOCR.Enums.Language.Lao, "lo"},
        {TesseractOCR.Enums.Language.Latin, "la"},
        {TesseractOCR.Enums.Language.Latvian, "lv"},
        {TesseractOCR.Enums.Language.Lithuanian, "lt"},
        {TesseractOCR.Enums.Language.Luxembourgish, "lb"},
        {TesseractOCR.Enums.Language.Malayalam, "ml"},
        {TesseractOCR.Enums.Language.Marathi, "mr"},
        {TesseractOCR.Enums.Language.Macedonian, "mk"},
        {TesseractOCR.Enums.Language.Maltese, "mt"},
        {TesseractOCR.Enums.Language.Mongolian, "mn"},
        {TesseractOCR.Enums.Language.Maori, "mi"},
        {TesseractOCR.Enums.Language.Malay, "ms"},
        {TesseractOCR.Enums.Language.Burmese, "my"},
        {TesseractOCR.Enums.Language.Nepali, "ne"},
        {TesseractOCR.Enums.Language.Dutch, "nl"},
        {TesseractOCR.Enums.Language.Norwegian, "no"},
        {TesseractOCR.Enums.Language.Occitan, "oc"},
        {TesseractOCR.Enums.Language.Oriya, "or"},
        //{TesseractOCR.Enums.Language.Osd, ""},
        {TesseractOCR.Enums.Language.Panjabi, "pa"},
        {TesseractOCR.Enums.Language.Polish, "pl"},
        {TesseractOCR.Enums.Language.Portuguese, "pt"},
        {TesseractOCR.Enums.Language.Pushto, "ps"},
        {TesseractOCR.Enums.Language.Quechua, "qu"},
        {TesseractOCR.Enums.Language.Romanian, "ro"},
        {TesseractOCR.Enums.Language.Russian, "ru"},
        {TesseractOCR.Enums.Language.Sanskrit, "sa"},
        {TesseractOCR.Enums.Language.Sinhala, "si"},
        {TesseractOCR.Enums.Language.Slovak, "sk"},
        //{TesseractOCR.Enums.Language.SlovakFrakturContrib, ""},
        {TesseractOCR.Enums.Language.Slovenian, "sl"},
        {TesseractOCR.Enums.Language.Sindhi, "sd"},
        {TesseractOCR.Enums.Language.SpanishCastilian, "es"},
        //{TesseractOCR.Enums.Language.SpanishCastilianOld, ""},
        {TesseractOCR.Enums.Language.Albanian, "sq"},
        {TesseractOCR.Enums.Language.Serbian, "sr"},
        //{TesseractOCR.Enums.Language.SerbianLatin, ""},
        {TesseractOCR.Enums.Language.Sundanese, "su"},
        {TesseractOCR.Enums.Language.Swahili, "sw"},
        {TesseractOCR.Enums.Language.Swedish, "sv"},
        //{TesseractOCR.Enums.Language.Syriac, ""},
        {TesseractOCR.Enums.Language.Tamil, "ta"},
        {TesseractOCR.Enums.Language.Tatar, "tt"},
        {TesseractOCR.Enums.Language.Telugu, "te"},
        {TesseractOCR.Enums.Language.Tajik, "tg"},
        {TesseractOCR.Enums.Language.Tagalog, "tl"},
        {TesseractOCR.Enums.Language.Thai, "th"},
        {TesseractOCR.Enums.Language.Tigrinya, "ti"},
        {TesseractOCR.Enums.Language.Tonga, "to"},
        {TesseractOCR.Enums.Language.Turkish, "tr"},
        {TesseractOCR.Enums.Language.Uighur, "ug"},
        {TesseractOCR.Enums.Language.Ukrainian, "uk"},
        {TesseractOCR.Enums.Language.Urdu, "ur"},
        {TesseractOCR.Enums.Language.Uzbek, "uz"},
        //{TesseractOCR.Enums.Language.UzbekCyrilic, ""},
        {TesseractOCR.Enums.Language.Vietnamese, "vi"},
        {TesseractOCR.Enums.Language.Yiddish, "yi"},
        {TesseractOCR.Enums.Language.Yoruba, "yo"},
    };

    public static string ModelsDirectory { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tesseractmodels", "tessdata");

    public TesseractOCR.Enums.Language Lang { get; set; }

    public string ISO6391 => TesseractLangToISO6391[Lang];

    public string LangCode =>
        TesseractOCR.Enums.LanguageHelper.EnumToString(Lang);

    public long Size
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(Downloaded));
            }
        }
    }

    public string ModelFileName => $"{LangCode}.traineddata";

    public string ModelFilePath => Path.Combine(ModelsDirectory, ModelFileName);

    public bool Downloaded => Size > 0;

    public override string ToString() => LangCode;

    public bool Equals(TesseractModel? other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        return Lang == other.Lang;
    }

    public override bool Equals(object? obj) => obj is TesseractModel o && Equals(o);

    public override int GetHashCode()
    {
        return (int)Lang;
    }
}

public class TesseractModelLoader
{
    /// <summary>
    /// key: ISO6391, value: TesseractModel
    /// Chinese has multiple models for one ISO6391 key
    /// </summary>
    /// <returns></returns>
    internal static Dictionary<string, List<TesseractModel>> GetAvailableModels()
    {
        List<TesseractModel> models = LoadDownloadedModels();

        Dictionary<string, List<TesseractModel>> dict = new();

        foreach (TesseractModel model in models)
        {
            if (TesseractModel.TesseractLangToISO6391.TryGetValue(model.Lang, out string? iso6391))
            {
                if (dict.ContainsKey(iso6391))
                {
                    // for chinese (zh-CN, zh-TW)
                    dict[iso6391].Add(model);
                }
                else
                {
                    dict.Add(iso6391, [model]);
                }
            }
        }

        return dict;
    }

    public static List<TesseractModel> LoadAllModels()
    {
        EnsureModelsDirectory();

        List<TesseractModel> models = Enum.GetValues<TesseractOCR.Enums.Language>()
            .Where(l => TesseractModel.TesseractLangToISO6391.ContainsKey(l))
            .Select(l => new TesseractModel { Lang = l })
            .OrderBy(m => m.Lang.ToString())
            .ToList();

        foreach (TesseractModel model in models)
        {
            // Initialize download status of each model
            string path = model.ModelFilePath;
            if (File.Exists(path))
            {
                model.Size = new FileInfo(path).Length;
            }
        }

        return models;
    }

    public static List<TesseractModel> LoadDownloadedModels()
    {
        return LoadAllModels()
            .Where(m => m.Downloaded)
            .ToList();
    }

    private static void EnsureModelsDirectory()
    {
        if (!Directory.Exists(TesseractModel.ModelsDirectory))
        {
            Directory.CreateDirectory(TesseractModel.ModelsDirectory);
        }
    }
}

using System.Diagnostics;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Navigation;

namespace LLPlayer.Extensions;

public static class HyperlinkHelper
{
    public static readonly DependencyProperty OpenInBrowserProperty =
        DependencyProperty.RegisterAttached(
            "OpenInBrowser",
            typeof(bool),
            typeof(HyperlinkHelper),
            new PropertyMetadata(false, OnOpenInBrowserChanged));

    public static bool GetOpenInBrowser(DependencyObject obj)
    {
        return (bool)obj.GetValue(OpenInBrowserProperty);
    }

    public static void SetOpenInBrowser(DependencyObject obj, bool value)
    {
        obj.SetValue(OpenInBrowserProperty, value);
    }

    private static void OnOpenInBrowserChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Hyperlink hyperlink)
        {
            bool newValue = (bool)e.NewValue;
            if (newValue)
            {
                hyperlink.RequestNavigate += OnRequestNavigate;
            }
            else
            {
                hyperlink.RequestNavigate -= OnRequestNavigate;
            }
        }
    }

    private static void OnRequestNavigate(object sender, RequestNavigateEventArgs e)
    {
        OpenUrlInBrowser(e.Uri.AbsoluteUri);
        e.Handled = true;
    }

    public static void OpenUrlInBrowser(string url)
    {
        Process.Start(new ProcessStartInfo
        {
            FileName = url,
            UseShellExecute = true
        });
    }
}

using System.IO;
using System.Text;

namespace LLPlayer.Services;

public static class SrtExporter
{
    // TODO: L: Supports tags such as <i>?
    public static void ExportSrt(List<SubtitleLine> lines, string filePath, Encoding encoding)
    {
        using StreamWriter writer = new(filePath, false, encoding);

        foreach (var (i, line) in lines.Index())
        {
            writer.WriteLine((i + 1).ToString());
            writer.WriteLine($"{FormatTime(line.Start)} --> {FormatTime(line.End)}");
            writer.WriteLine(line.Text);
            // blank line expect last
            if (i != lines.Count - 1)
            {
                writer.WriteLine();
            }
        }
    }

    private static string FormatTime(TimeSpan time)
    {
        return string.Format("{0:00}:{1:00}:{2:00},{3:000}",
            (int)time.TotalHours,
            time.Minutes,
            time.Seconds,
            time.Milliseconds);
    }
}

public class SubtitleLine
{
    public required TimeSpan Start { get; init; }
    public required TimeSpan End { get; init; }
    public required string Text { get; init; }
}

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;

using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaFramework.MediaStream;

using static FlyleafLib.Utils;

namespace FlyleafLib.Plugins;

public abstract class PluginBase : PluginType, IPlugin
{
    public ObservableDictionary<string, string>
                                    Options         => Config?.Plugins[Name];
    public Config                   Config          => Handler.Config;

    public Playlist                 Playlist        => Handler.Playlist;
    public PlaylistItem             Selected        => Handler.Playlist.Selected;

    public DecoderContext           decoder         => (DecoderContext) Handler;

    public PluginHandler            Handler         { get; internal set; }
    public LogHandler               Log             { get; internal set; }
    public bool                     Disposed        { get; protected set;} = true;
    public int                      Priority        { get; set; } = 1000;

    public virtual void OnLoaded() { }
    public virtual void OnInitializing() { }
    public virtual void OnInitialized() { }

    public virtual void OnInitializingSwitch() { }
    public virtual void OnInitializedSwitch() { }

    public virtual void OnBuffering() { }
    public virtual void OnBufferingCompleted() { }

    public virtual void OnOpen() { }
    public virtual void OnOpenExternalAudio() { }
    public virtual void OnOpenExternalVideo() { }
    public virtual void OnOpenExternalSubtitles() { }

    public virtual void Dispose() { }

    public void AddExternalStream(ExternalStream extStream, object tag = null, PlaylistItem item = null)
    {
        item ??= Playlist.Selected;

        if (item != null)
            PlaylistItem.AddExternalStream(extStream, item, Name, tag);
    }

    public void AddPlaylistItem(PlaylistItem item, object tag = null)
        => Playlist.AddItem(item, Name, tag);

    public void AddTag(object tag, PlaylistItem item = null)
    {
        item ??= Playlist.Selected;

        item?.AddTag(tag, Name);
    }

    public object GetTag(ExternalStream extStream)
        => extStream?.GetTag(Name);

    public object GetTag(PlaylistItem item)
        => item?.GetTag(Name);

    public virtual Dictionary<string, string> GetDefaultOptions() => new();
}
public class PluginType
{
    public Type                     Type            { get; internal set; }
    public string                   Name            { get; internal set; }
    public Version                  Version         { get; internal set; }
}
public class OpenResults
{
    public string   Error;
    public bool     Success => Error == null;

    public OpenResults() { }
    public OpenResults(string error) => Error = error;
}

public class OpenSubtitlesResults : OpenResults
{
    public ExternalSubtitlesStream ExternalSubtitlesStream;
    public OpenSubtitlesResults(ExternalSubtitlesStream extStream, string error = null) : base(error) => ExternalSubtitlesStream = extStream;
}

public interface IPlugin : IDisposable
{
    string          Name        { get; }
    Version         Version     { get; }
    PluginHandler   Handler     { get; }
    int             Priority    { get; }

    void OnLoaded();
    void OnInitializing();
    void OnInitialized();
    void OnInitializingSwitch();
    void OnInitializedSwitch();

    void OnBuffering();
    void OnBufferingCompleted();

    void OnOpenExternalAudio();
    void OnOpenExternalVideo();
    void OnOpenExternalSubtitles();
}

public interface IOpen : IPlugin
{
    bool CanOpen();
    OpenResults     Open();
    OpenResults     OpenItem();
}
public interface IOpenSubtitles : IPlugin
{
    OpenSubtitlesResults Open(string url);
    OpenSubtitlesResults Open(Stream iostream);
}

public interface IScrapeItem : IPlugin
{
    void ScrapeItem(PlaylistItem item);
}

public interface ISuggestPlaylistItem : IPlugin
{
    PlaylistItem SuggestItem();
}

public interface ISuggestExternalAudio : IPlugin
{
    ExternalAudioStream SuggestExternalAudio();
}
public interface ISuggestExternalVideo : IPlugin
{
    ExternalVideoStream SuggestExternalVideo();
}

public interface ISuggestAudioStream : IPlugin
{
    AudioStream SuggestAudio(ObservableCollection<AudioStream> streams);
}
public interface ISuggestVideoStream : IPlugin
{
    VideoStream SuggestVideo(ObservableCollection<VideoStream> streams);
}

public interface ISuggestSubtitlesStream : IPlugin
{
    SubtitlesStream SuggestSubtitles(ObservableCollection<SubtitlesStream> streams, List<Language> langs);
}

public interface ISuggestSubtitles : IPlugin
{
    /// <summary>
    /// Suggests from all the available subtitles
    /// </summary>
    /// <param name="stream">Embedded stream</param>
    /// <param name="extStream">External stream</param>
    void SuggestSubtitles(out SubtitlesStream stream, out ExternalSubtitlesStream extStream);
}

public interface ISuggestBestExternalSubtitles : IPlugin
{
    /// <summary>
    /// Suggests only if best match exists (to avoid search local/online)
    /// </summary>
    /// <returns></returns>
    ExternalSubtitlesStream SuggestBestExternalSubtitles();
}

public interface ISearchLocalSubtitles : IPlugin
{
    void SearchLocalSubtitles();
}

public interface ISearchOnlineSubtitles : IPlugin
{
    void SearchOnlineSubtitles();
}

public interface IDownloadSubtitles : IPlugin
{
    bool DownloadSubtitles(ExternalSubtitlesStream extStream);
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace LLPlayer.Extensions;

public static class FocusBehavior
{
    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached(
            "IsFocused",
            typeof(bool),
            typeof(FocusBehavior),
            new UIPropertyMetadata(false, OnIsFocusedChanged));

    public static bool GetIsFocused(DependencyObject obj) =>
        (bool)obj.GetValue(IsFocusedProperty);

    public static void SetIsFocused(DependencyObject obj, bool value) =>
        obj.SetValue(IsFocusedProperty, value);

    private static void OnIsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is UIElement element) || !(e.NewValue is bool isFocused) || !isFocused)
            return;

        // Set focus to element
        element.Dispatcher.BeginInvoke(() =>
        {
            element.Focus();
            if (element is TextBox tb)
            {
                // if TextBox, then select text
                tb.SelectAll();
                //tb.CaretIndex = tb.Text.Length;
            }
        }, DispatcherPriority.Input);
    }
}

using System.Linq;

namespace FlyleafLib.MediaFramework.MediaStream;

public class ExternalSubtitlesStream : ExternalStream, ISubtitlesStream
{
    public SelectedSubMethod[] SelectedSubMethods
    {
        get
        {
            var methods = (SelectSubMethod[])Enum.GetValues(typeof(SelectSubMethod));

            if (!IsBitmap)
            {
                // delete OCR if text sub
                methods = methods.Where(m => m != SelectSubMethod.OCR).ToArray();
            }

            return methods.
                Select(m => new SelectedSubMethod(this, m)).ToArray();
        }
    }

    public bool     IsBitmap        { get; set; }
    public bool     ManualDownloaded{ get; set; }
    public bool     Automatic       { get; set; }
    public bool     Downloaded      { get; set; }
    public Language Language        { get; set; } = Language.Unknown;
    public bool     LanguageDetected{ get; set; }
    // TODO: Add confidence rating (maybe result is for other movie/episode) | Add Weight calculated based on rating/downloaded/confidence (and lang?) which can be used from suggesters
    public string   Title           { get; set; }

    public string   DisplayMember =>
        $"({Language}){(ManualDownloaded ? " (DL)" : "")}{(Automatic ? " (Auto)" : "")} {Utils.TruncateString(Title, 50)} ({(IsBitmap ? "BMP" : "TXT")})";
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class SubtitlesDownloaderDialog : UserControl
{
    public SubtitlesDownloaderDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SubtitlesDownloaderDialogVM>();
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class SubtitlesExportDialog : UserControl
{
    public SubtitlesExportDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<SubtitlesExportDialogVM>();
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class WhisperEngineDownloadDialog : UserControl
{
    public WhisperEngineDownloadDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<WhisperEngineDownloadDialogVM>();
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;

public partial class WhisperModelDownloadDialog : UserControl
{
    public WhisperModelDownloadDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<WhisperModelDownloadDialogVM>();
    }
}

using System.Collections;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using LLPlayer.ViewModels;

namespace LLPlayer.Controls.Settings;

public partial class SettingsPlugins : UserControl
{
    public SettingsPlugins()
    {
        InitializeComponent();
    }

    private void PluginValueChanged(object sender, RoutedEventArgs e)
    {
        string curPlugin = ((TextBlock)((Panel)((FrameworkElement)sender).Parent).Children[0]).Text;

        if (DataContext is SettingsDialogVM vm)
        {
            vm.FL.PlayerConfig.Plugins[cmbPlugins.Text][curPlugin] = ((TextBox)sender).Text;
        }
    }
}

public class GetDictionaryItemConverter : IMultiValueConverter
{
    public object? Convert(object[]? value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
            return null;
        if (value[0] is not IDictionary dictionary)
            return null;
        if (value[1] is not string key)
            return null;

        return dictionary[key];
    }
    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Views;
public partial class TesseractDownloadDialog : UserControl
{
    public TesseractDownloadDialog()
    {
        InitializeComponent();

        DataContext = ((App)Application.Current).Container.Resolve<TesseractDownloadDialogVM>();
    }
}

using System.Threading;
using System.Threading.Tasks;
using DeepL;
using DeepL.Model;

namespace FlyleafLib.MediaPlayer.Translation.Services;

#nullable enable

public class DeepLTranslateService : ITranslateService
{
    private string? _srcLang;
    private string? _targetLang;
    private readonly Translator _translator;
    private readonly DeepLTranslateSettings _settings;

    public DeepLTranslateService(DeepLTranslateSettings settings)
    {
        if (string.IsNullOrWhiteSpace(settings.ApiKey))
        {
            throw new TranslationConfigException(
                $"API Key for {ServiceType} is not configured.");
        }

        _settings = settings;
        _translator = new Translator(_settings.ApiKey, new TranslatorOptions()
        {
            OverallConnectionTimeout = TimeSpan.FromMilliseconds(settings.TimeoutMs)
        });
    }

    public TranslateServiceType ServiceType => TranslateServiceType.DeepL;

    public void Dispose()
    {
        _translator.Dispose();
    }

    public void Initialize(Language src, TargetLanguage target)
    {
        (TranslateLanguage srcLang, _) = this.TryGetLanguage(src, target);

        _srcLang = ToSourceCode(srcLang.ISO6391);
        _targetLang = ToTargetCode(target);
    }

    internal static string ToSourceCode(string iso6391)
    {
        // ref: https://developers.deepl.com/docs/resources/supported-languages

        // Just capitalize ISO6391.
        return iso6391.ToUpper();
    }

    internal static string ToTargetCode(TargetLanguage target)
    {
        return target switch
        {
            TargetLanguage.EnglishAmerican => "EN-US",
            TargetLanguage.EnglishBritish => "EN-GB",
            TargetLanguage.Portuguese => "PT-PT",
            TargetLanguage.PortugueseBrazilian => "PT-BR",
            TargetLanguage.ChineseSimplified => "ZH-HANS",
            TargetLanguage.ChineseTraditional => "ZH-HANT",
            _ => target.ToISO6391().ToUpper()
        };
    }

    public async Task<string> TranslateAsync(string text, CancellationToken token)
    {
        if (_srcLang == null || _targetLang == null)
            throw new InvalidOperationException("must be initialized");

        try
        {
            TextResult result = await _translator.TranslateTextAsync(text, _srcLang, _targetLang,
                new TextTranslateOptions
                {
                    Formality = Formality.Default,
                }, token).ConfigureAwait(false);

            return result.Text;
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            // Timeout: DeepL.ConnectionException
            throw new TranslationException($"Cannot request to {ServiceType}: {ex.Message}", ex);
        }
    }
}

using System.IO;
using static FlyleafLib.Utils;

namespace LLPlayer.Extensions;

public static class FileHelper
{
    /// <summary>
    /// Retrieves the next and previous file from the specified file path.
    /// Select files with the same extension in the same folder, sorted in natural alphabetical order.
    /// Returns null if the next or previous file does not exist.
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    /// <exception cref="FileNotFoundException"></exception>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="InvalidOperationException"></exception>
    public static (string? prev, string? next) GetNextAndPreviousFile(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("file does not exist", filePath);
        }

        string? directory = Path.GetDirectoryName(filePath);
        string? extension = Path.GetExtension(filePath);

        if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(extension))
        {
            throw new InvalidOperationException($"filePath is invalid: {filePath}");
        }

        // Get files with the same extension, ignoring case
        List<string> foundFiles = Directory.GetFiles(directory, $"*{extension}")
                                  .Where(f => string.Equals(Path.GetExtension(f), extension, StringComparison.OrdinalIgnoreCase))
                                  .OrderBy(f => Path.GetFileName(f), new NaturalStringComparer())
                                  .ToList();

        if (foundFiles.Count == 0)
        {
            throw new InvalidOperationException($"same extension file does not exist: {filePath}");
        }

        int currentIndex = foundFiles.FindIndex(f => string.Equals(f, filePath, StringComparison.OrdinalIgnoreCase));
        if (currentIndex == -1)
        {
            throw new InvalidOperationException($"current file does not exist: {filePath}");
        }

        string? next = (currentIndex < foundFiles.Count - 1) ? foundFiles[currentIndex + 1] : null;
        string? prev = (currentIndex > 0) ? foundFiles[currentIndex - 1] : null;

        return (prev, next);
    }
}

using System.Collections.Generic;

using FlyleafLib.MediaFramework.MediaDemuxer;
using FlyleafLib.MediaFramework.MediaPlaylist;
using FlyleafLib.MediaPlayer;

namespace FlyleafLib.MediaFramework.MediaStream;

public class ExternalStream : DemuxerInput
{
    public string   PluginName      { get; set; }
    public PlaylistItem
                    PlaylistItem    { get; set; }
    public int      Index           { get; set; } = -1; // if we need it (already used to compare same type streams) we need to ensure we fix it in case of removing an item
    public string   Protocol        { get; set; }
    public string   Codec           { get; set; }
    public long     BitRate         { get; set; }
    public Dictionary<string, object>
                    Tag             { get; set; } = new Dictionary<string, object>();
    public void AddTag(object tag, string pluginName)
    {
        if (Tag.ContainsKey(pluginName))
            Tag[pluginName] = tag;
        else
            Tag.Add(pluginName, tag);
    }
    public object GetTag(string pluginName)
        => Tag.ContainsKey(pluginName) ? Tag[pluginName] : null;

    /// <summary>
    /// Whether the item is currently enabled or not
    /// </summary>
    public bool Enabled
    {
        get => _Enabled;
        set
        {
            Utils.UI(() =>
            {
                if (Set(ref _Enabled, value) && value)
                {
                    OpenedCounter++;
                }

                Raise(nameof(EnabledPrimarySubtitle));
                Raise(nameof(EnabledSecondarySubtitle));
                Raise(nameof(SubtitlesStream.SelectedSubMethods));
            });
        }
    }
    bool _Enabled;

    /// <summary>
    /// Times this item has been used/opened
    /// </summary>
    public int      OpenedCounter   { get; set; }

    public MediaType
                    Type => this is ExternalAudioStream ? MediaType.Audio : this is ExternalVideoStream ? MediaType.Video : MediaType.Subs;

    #region Subtitles
    // TODO: L: Used for subtitle streams only, but defined in the base class
    public bool EnabledPrimarySubtitle => Enabled && this.GetSubEnabled(0);
    public bool EnabledSecondarySubtitle => Enabled && this.GetSubEnabled(1);
    #endregion
}

using System;
using System.Linq;

using Vortice.MediaFoundation;

namespace FlyleafLib.MediaFramework.MediaDevice;

public class VideoDevice : DeviceBase<VideoDeviceStream>
{
    public VideoDevice(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink)
    {
        Streams = VideoDeviceStream.GetVideoFormatsForVideoDevice(friendlyName, symbolicLink);
        Url = Streams.Where(f => f.SubType.Contains("MJPG") && f.FrameRate >= 30).OrderByDescending(f => f.FrameSizeHeight).FirstOrDefault()?.Url;
    }

    public static void RefreshDevices()
    {
        Utils.UIInvokeIfRequired(() =>
        {
            Engine.Video.CapDevices.Clear();

            var devices = MediaFactory.MFEnumVideoDeviceSources();
                foreach (var device in devices)
                try { Engine.Video.CapDevices.Add(new VideoDevice(device.FriendlyName, device.SymbolicLink)); } catch(Exception) { }
        });
    }
}

using System.IO;
using System.Windows;
using FlyleafLib;
using FlyleafLib.MediaPlayer;
using FlyleafLib.MediaPlayer.Translation;

namespace LLPlayer.Services;

public static class FlyleafLoader
{
    public static void StartEngine()
    {
        EngineConfig engineConfig = DefaultEngineConfig();

        // Load Player's Config
        if (File.Exists(App.EngineConfigPath))
        {
            try
            {
                var opts = AppConfig.GetJsonSerializerOptions();
                engineConfig = EngineConfig.Load(App.EngineConfigPath, opts);
                if (engineConfig.Version != App.Version)
                {
                    engineConfig.Version = App.Version;
                    engineConfig.Save(App.EngineConfigPath, opts);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Cannot load EngineConfig from {Path.GetFileName(App.EngineConfigPath)}, Please review the settings or delete the config file. Error details are recorded in {Path.GetFileName(App.CrashLogPath)}.");
                try
                {
                    File.WriteAllText(App.CrashLogPath, "EngineConfig Loading Error: " + ex);
                }
                catch
                {
                    // ignored
                }

                Application.Current.Shutdown();
            }
        }

        Engine.Start(engineConfig);
    }

    public static Player CreateFlyleafPlayer()
    {
        Config? config = null;
        bool useConfig = false;

        // Load Player's Config
        if (File.Exists(App.PlayerConfigPath))
        {
            try
            {
                var opts = AppConfig.GetJsonSerializerOptions();
                config = Config.Load(App.PlayerConfigPath, opts);

                if (config.Version != App.Version)
                {
                    config.Version = App.Version;
                    config.Save(App.PlayerConfigPath, opts);
                }
                useConfig = true;
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Cannot load PlayerConfig from {Path.GetFileName(App.PlayerConfigPath)}, Please review the settings or delete the config file. Error details are recorded in {Path.GetFileName(App.CrashLogPath)}.");
                try
                {
                    File.WriteAllText(App.CrashLogPath, "PlayerConfig Loading Error: " + ex);
                }
                catch
                {
                    // ignored
                }

                Application.Current.Shutdown();
            }
        }

        config ??= DefaultConfig();
        Player player = new(config);

        if (!useConfig)
        {
            // Initialize default key bindings for custom keys for new config.
            foreach (var binding in AppActions.DefaultCustomActionsMap())
            {
                config.Player.KeyBindings.Keys.Add(binding);
            }
        }

        return player;
    }

    public static EngineConfig DefaultEngineConfig()
    {
        EngineConfig engineConfig = new()
        {
#if DEBUG
            PluginsPath = @":Plugins\bin\Plugins.NET9",
#else
            PluginsPath = ":Plugins",
#endif
            FFmpegPath = ":FFmpeg",
            FFmpegHLSLiveSeek = true,
            UIRefresh = true,
            FFmpegLoadProfile = Flyleaf.FFmpeg.LoadProfile.Filters,
#if DEBUG
            LogOutput = ":debug",
            LogLevel = LogLevel.Debug,
            FFmpegLogLevel = Flyleaf.FFmpeg.LogLevel.Warn,
#endif
        };

        return engineConfig;
    }

    private static Config DefaultConfig()
    {
        Config config = new();
        config.Demuxer.FormatOptToUnderlying =
            true; // Mainly for HLS to pass the original query which might includes session keys
        config.Audio.FiltersEnabled = true; // To allow embedded atempo filter for speed
        config.Video.GPUAdapter = ""; // Set it empty so it will include it when we save it
        config.Subtitles.SearchLocal = true;
        config.Subtitles.TranslateTargetLanguage = Language.Get(Utils.OriginalCulture).ToTargetLanguage() ?? TargetLanguage.EnglishAmerican; // try to set native language

        return config;
    }
}

using System.Diagnostics;

using static FlyleafLib.Logger;

namespace FlyleafLib.MediaFramework.MediaDemuxer;

public unsafe class Interrupter
{
    public int          ForceInterrupt  { get; set; }
    public Requester    Requester       { get; private set; }
    public int          Interrupted     { get; private set; }
    public bool         Timedout        { get; private set; }

    Demuxer demuxer;
    Stopwatch sw = new();
    internal AVIOInterruptCB_callback interruptClbk;
    long curTimeoutMs;

    internal int ShouldInterrupt(void* opaque)
    {
        if (demuxer.Status == Status.Stopping)
        {
            if (CanDebug) demuxer.Log.Debug($"{Requester} Interrupt (Stopping) !!!");

            return Interrupted = 1;
        }

        if (demuxer.Config.AllowTimeouts && sw.ElapsedMilliseconds > curTimeoutMs)
        {
            if (Timedout)
                return Interrupted = 1;

            if (CanWarn) demuxer.Log.Warn($"{Requester} Timeout !!!! {sw.ElapsedMilliseconds} ms");

            Timedout    = true;
            Interrupted = 1;
            demuxer.OnTimedOut();

            return Interrupted;
        }

        if (Requester == Requester.Close)
            return 0;

        if (ForceInterrupt != 0 && demuxer.allowReadInterrupts)
        {
            if (CanTrace) demuxer.Log.Trace($"{Requester} Interrupt !!!");

            return Interrupted = 1;
        }

        return Interrupted = 0;
    }

    public Interrupter(Demuxer demuxer)
    {
        this.demuxer    = demuxer;
        interruptClbk   = ShouldInterrupt;
    }

    public void ReadRequest()
    {
        Requester   = Requester.Read;

        if (!demuxer.Config.AllowTimeouts)
            return;

        Timedout    = false;
        curTimeoutMs= demuxer.IsLive ? demuxer.Config.readLiveTimeoutMs: demuxer.Config.readTimeoutMs;
        sw.Restart();
    }

    public void SeekRequest()
    {
        Requester   = Requester.Seek;

        if (!demuxer.Config.AllowTimeouts)
            return;

        Timedout    = false;
        curTimeoutMs= demuxer.Config.seekTimeoutMs;
        sw.Restart();
    }

    public void OpenRequest()
    {
        Requester   = Requester.Open;

        if (!demuxer.Config.AllowTimeouts)
            return;

        Timedout    = false;
        curTimeoutMs= demuxer.Config.openTimeoutMs;
        sw.Restart();
    }

    public void CloseRequest()
    {
        Requester   = Requester.Close;

        if (!demuxer.Config.AllowTimeouts)
            return;

        Timedout    = false;
        curTimeoutMs= demuxer.Config.closeTimeoutMs;
        sw.Restart();
    }
}

public enum Requester
{
    Close,
    Open,
    Read,
    Seek
}

using System.IO;
using System.Text.Json.Serialization;
using Whisper.net.Ggml;

namespace FlyleafLib;

#nullable enable

public class WhisperCppModel : NotifyPropertyChanged, IEquatable<WhisperCppModel>
{
    public GgmlType Model { get; set; }

    [JsonIgnore]
    public long Size
    {
        get;
        set
        {
            if (Set(ref field, value))
            {
                Raise(nameof(Downloaded));
            }
        }
    }

    [JsonIgnore]
    public string ModelFileName
    {
        get
        {
            string modelName = Model.ToString().ToLower();
            return $"ggml-{modelName}.bin";
        }
    }

    [JsonIgnore]
    public string ModelFilePath => Path.Combine(WhisperConfig.ModelsDirectory, ModelFileName);

    [JsonIgnore]
    public bool Downloaded => Size > 0;

    public override string ToString() => Model.ToString();

    public bool Equals(WhisperCppModel? other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        return Model == other.Model;
    }

    public override bool Equals(object? obj) => obj is WhisperCppModel o && Equals(o);

    public override int GetHashCode()
    {
        return (int)Model;
    }
}

using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfColorFontDialog
{
	public class FontSizeListBoxItemToDoubleConverter : IValueConverter
	{
		public FontSizeListBoxItemToDoubleConverter()
		{
		}

		object System.Windows.Data.IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
		{
            string str = value.ToString();
            try
            {
                return double.Parse(value.ToString());
            }
            catch(FormatException)
            {
                return 0;
            }

        }

		object System.Windows.Data.IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
		{
			throw new NotImplementedException();
		}
	}
}

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;

namespace WpfColorFontDialog
{
    public class FontValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is FontFamily font)
            {
                var name = I18NUtil.CurrentLanguage;
                if (string.IsNullOrWhiteSpace(name))
                {
                    name = I18NUtil.GetCurrentLanguage();
                }
                var names = new[] { name, I18NUtil.GetLanguage() };
                foreach (var s in names)
                {
                    if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage(s), out var localizedName))
                    {
                        if (!string.IsNullOrEmpty(localizedName))
                        {
                            return localizedName;
                        }
                    }
                }

                return font.Source;
            }

            // Avoid reaching here and getting an error.
            return string.Empty;
            //throw new NotSupportedException();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("ConvertBack not supported");
        }
    }
}

using System.IO;

namespace FlyleafLib.MediaFramework.MediaDemuxer;

public unsafe class CustomIOContext
{
    AVIOContext*    avioCtx;
    public Stream   stream;
    Demuxer         demuxer;

    public CustomIOContext(Demuxer demuxer)
    {
        this.demuxer = demuxer;
    }

    public void Initialize(Stream stream)
    {
        this.stream = stream;
        //this.stream.Seek(0, SeekOrigin.Begin);

        ioread = IORead;
        ioseek = IOSeek;
        avioCtx = avio_alloc_context((byte*)av_malloc((nuint)demuxer.Config.IOStreamBufferSize), demuxer.Config.IOStreamBufferSize, 0, null, ioread, null, ioseek);
        demuxer.FormatContext->pb     = avioCtx;
        demuxer.FormatContext->flags |= FmtFlags2.CustomIo;
    }

    public void Dispose()
    {
        if (avioCtx != null)
        {
            av_free(avioCtx->buffer);
            fixed (AVIOContext** ptr = &avioCtx) avio_context_free(ptr);
        }
        avioCtx = null;
        stream = null;
        ioread = null;
        ioseek = null;
    }

    avio_alloc_context_read_packet  ioread;
    avio_alloc_context_seek         ioseek;

    int IORead(void* opaque, byte* buffer, int bufferSize)
    {
        int ret;

        if (demuxer.Interrupter.ShouldInterrupt(null) != 0) return AVERROR_EXIT;

        ret = demuxer.CustomIOContext.stream.Read(new Span<byte>(buffer, bufferSize));

        if (ret > 0)
            return ret;

        if (ret == 0)
            return AVERROR_EOF;

        demuxer.Log.Warn("CustomIOContext Interrupted");

        return AVERROR_EXIT;
    }

    long IOSeek(void* opaque, long offset, IOSeekFlags whence)
    {
        //System.Diagnostics.Debug.WriteLine($"** S | {decCtx.demuxer.fmtCtx->pb->pos} - {decCtx.demuxer.ioStream.Position}");

        return whence == IOSeekFlags.Size
            ? demuxer.CustomIOContext.stream.Length
            : demuxer.CustomIOContext.stream.Seek(offset, (SeekOrigin) whence);
    }
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using MaterialDesignThemes.Wpf;

namespace LLPlayer.Controls.Settings.Controls;

public partial class ColorPicker : UserControl
{
    public ColorPicker()
    {
        InitializeComponent();

        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        // save initial color for cancellation
        _initialColor = PickerColor;

        MyNamedColors.SelectedItem = null;
    }

    private Color? _initialColor = null;

    public Color PickerColor
    {
        get => (Color)GetValue(PickerColorProperty);
        set => SetValue(PickerColorProperty, value);
    }

    public static readonly DependencyProperty PickerColorProperty =
        DependencyProperty.Register(nameof(PickerColor), typeof(Color), typeof(ColorPicker));

    public List<KeyValuePair<string, Color>> NamedColors { get; } = GetColors();

    private static List<KeyValuePair<string, Color>> GetColors()
    {
        return typeof(Colors)
            .GetProperties()
            .Where(prop =>
                typeof(Color).IsAssignableFrom(prop.PropertyType))
            .Select(prop =>
                new KeyValuePair<string, Color>(prop.Name, (Color)prop.GetValue(null)!))
            .ToList();
    }

    private void NamedColors_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (MyNamedColors.SelectedItem != null)
        {
            PickerColor = ((KeyValuePair<string, Color>)MyNamedColors.SelectedItem).Value;
        }
    }

    private void ApplyButton_Click(object sender, RoutedEventArgs e)
    {
        DialogHost.CloseDialogCommand.Execute("apply", this);
    }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        if (_initialColor.HasValue)
        {
            PickerColor = _initialColor.Value;
        }
        DialogHost.CloseDialogCommand.Execute("cancel", this);
    }
}

using FlyleafLib.MediaFramework.MediaContext;
using FlyleafLib.MediaFramework.MediaStream;
using System;
using System.Collections.ObjectModel;

namespace FlyleafLib.MediaPlayer;
public class Data : NotifyPropertyChanged
{
    /// <summary>
    /// Embedded Streams
    /// </summary>
    public ObservableCollection<DataStream>
                        Streams => decoder?.DataDemuxer.DataStreams;

    /// <summary>
    /// Whether the input has data and it is configured
    /// </summary>
    public bool IsOpened { get => isOpened; internal set => Set(ref _IsOpened, value); }
    internal bool   _IsOpened, isOpened;

    Action uiAction;
    Player player;
    DecoderContext decoder => player.decoder;
    Config Config => player.Config;

    public Data(Player player)
    {
        this.player = player;
        uiAction = () =>
        {
            IsOpened = IsOpened;
        };
    }
    internal void Reset()
    {
        isOpened = false;

        player.UIAdd(uiAction);
    }
    internal void Refresh()
    {
        if (decoder.DataStream == null)
        { Reset(); return; }

        isOpened = !decoder.DataDecoder.Disposed;

        player.UIAdd(uiAction);
    }
    internal void Enable()
    {
        if (!player.CanPlay)
            return;

        decoder.OpenSuggestedData();
        player.ReSync(decoder.DataStream, (int)(player.CurTime / 10000), true);

        Refresh();
        player.UIAll();
    }
    internal void Disable()
    {
        if (!IsOpened)
            return;

        decoder.CloseData();

        player.dFrame = null;
        Reset();
        player.UIAll();
    }
}

using System.Globalization;
using System.Windows.Data;

namespace FlyleafLib.Controls.WPF;

[ValueConversion(typeof(long), typeof(TimeSpan))]
public class TicksToTimeSpanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)     => new TimeSpan((long)value);
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => ((TimeSpan)value).Ticks;
}

[ValueConversion(typeof(long), typeof(string))]
public class TicksToTimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)     => new TimeSpan((long)value).ToString(@"hh\:mm\:ss");
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

[ValueConversion(typeof(long), typeof(double))]
public class TicksToSecondsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)     => (long)value / 10000000.0;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => (long)((double)value * 10000000);
}

[ValueConversion(typeof(long), typeof(int))]
public class TicksToMilliSecondsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)     => (int)((long)value / 10000);

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => long.Parse(value.ToString()) * 10000;
}

[ValueConversion(typeof(AspectRatio), typeof(string))]
public class StringToRationalConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)     => value.ToString();

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => new AspectRatio(value.ToString());
}

using System.IO;
using System.Windows;
using FlyleafLib;
using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaPlayer;

namespace LLPlayer.Services;

public class FlyleafManager
{
    public Player Player { get; }
    public Config PlayerConfig => Player.Config;
    public FlyleafHost? FlyleafHost => Player.Host as FlyleafHost;
    public AppConfig Config { get; }
    public AppActions Action { get; }

    public AudioEngine AudioEngine => Engine.Audio;
    public EngineConfig ConfigEngine => Engine.Config;

    public FlyleafManager(Player player, IDialogService dialogService)
    {
        Player = player;

        // Load app configuration at this time
        Config = LoadAppConfig();
        Action = new AppActions(Player, Config, dialogService);
    }

    private AppConfig LoadAppConfig()
    {
        AppConfig? config = null;

        if (File.Exists(App.AppConfigPath))
        {
            try
            {
                config = AppConfig.Load(App.AppConfigPath);

                if (config.Version != App.Version)
                {
                    config.Version = App.Version;
                    config.Save(App.AppConfigPath);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Cannot load AppConfig from {Path.GetFileName(App.AppConfigPath)}, Please review the settings or delete the config file. Error details are recorded in {Path.GetFileName(App.CrashLogPath)}.");
                try
                {
                    File.WriteAllText(App.CrashLogPath, "AppConfig Loading Error: " + ex);
                }
                catch
                {
                    // ignored
                }

                Application.Current.Shutdown();
            }
        }

        if (config == null)
        {
            config = new AppConfig();
        }
        config.Initialize(this);

        return config;
    }
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

using FlyleafLib.MediaPlayer;

namespace FlyleafLib.Controls.WPF;

public partial class PlayerDebug : UserControl
{
    public Player Player
    {
        get => (Player)GetValue(PlayerProperty);
        set => SetValue(PlayerProperty, value);
    }

    public static readonly DependencyProperty PlayerProperty =
        DependencyProperty.Register("Player", typeof(Player), typeof(PlayerDebug), new PropertyMetadata(null));

    public Brush BoxColor
    {
        get => (Brush)GetValue(BoxColorProperty);
        set => SetValue(BoxColorProperty, value);
    }

    public static readonly DependencyProperty BoxColorProperty =
        DependencyProperty.Register("BoxColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D0000000"))));

    public Brush HeaderColor
    {
        get => (Brush)GetValue(HeaderColorProperty);
        set => SetValue(HeaderColorProperty, value);
    }

    public static readonly DependencyProperty HeaderColorProperty =
        DependencyProperty.Register("HeaderColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.LightSalmon)));

    public Brush InfoColor
    {
        get => (Brush)GetValue(InfoColorProperty);
        set => SetValue(InfoColorProperty, value);
    }

    public static readonly DependencyProperty InfoColorProperty =
        DependencyProperty.Register("InfoColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.LightSteelBlue)));

    public Brush ValueColor
    {
        get => (Brush)GetValue(ValueColorProperty);
        set => SetValue(ValueColorProperty, value);
    }

    public static readonly DependencyProperty ValueColorProperty =
        DependencyProperty.Register("ValueColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.White)));

    public PlayerDebug() => InitializeComponent();
}

namespace FlyleafLib.MediaPlayer.Translation;

public class TranslateChatConfig : NotifyPropertyChanged
{
    public const string DefaultPromptOneByOne =
        """
        You are a professional subtitle translator, translate from {source_lang} to {target_lang}, keep sentences in {target_lang} as they are, censoring of translations is prohibited, return only the translated text without the sent text or notes or comments or anything:

        {source_text}
        """;

    public const string DefaultPromptKeepContext =
        """
        You are a professional subtitle translator.
        I will send the text of the subtitles of the video one at a time.
        Please translate the text while taking into account the context of the previous text.

        Translate from {source_lang} to {target_lang}.
        Return only the translated text without the sent text or notes or comments or anything.
        Keep sentences in {target_lang} as they are.
        Censoring of translations is prohibited.
        """;

    public string PromptOneByOne { get; set => Set(ref field, value); } = DefaultPromptOneByOne.ReplaceLineEndings("\n");

    public string PromptKeepContext { get; set => Set(ref field, value); } = DefaultPromptKeepContext.ReplaceLineEndings("\n");

    public ChatTranslateMethod TranslateMethod { get; set => Set(ref field, value); } = ChatTranslateMethod.KeepContext;

    public int SubtitleContextCount { get; set => Set(ref field, value); } = 6;

    public ChatContextRetainPolicy ContextRetainPolicy { get; set => Set(ref field, value); } = ChatContextRetainPolicy.Reset;

    public bool IncludeTargetLangRegion { get; set => Set(ref field, value); } = true;
}

public enum ChatTranslateMethod
{
    KeepContext,
    OneByOne
}

public enum ChatContextRetainPolicy
{
    Reset,
    KeepSize
}

using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;

namespace LLPlayer.Resources;

public partial class Validators : ResourceDictionary
{
    public Validators()
    {
        InitializeComponent();
    }
}

public class ColorHexRule : ValidationRule
{
    public override ValidationResult Validate(object? value, CultureInfo cultureInfo)
    {
        if (value != null && Regex.IsMatch(value.ToString() ?? string.Empty, "^[0-9a-f]{6}$", RegexOptions.IgnoreCase))
        {
            return new ValidationResult(true, null);
        }

        return new ValidationResult(false, "Invalid");
    }
}

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Media;

namespace WpfColorFontDialog
{
	internal class AvailableColors : List<FontColor>
	{
		public AvailableColors()
		{
			this.Init();
		}

		public static FontColor GetFontColor(SolidColorBrush b)
		{
			return (new AvailableColors()).GetFontColorByBrush(b);
		}

		public static FontColor GetFontColor(string name)
		{
			return (new AvailableColors()).GetFontColorByName(name);
		}

		public static FontColor GetFontColor(Color c)
		{
			return AvailableColors.GetFontColor(new SolidColorBrush(c));
		}

		public FontColor GetFontColorByBrush(SolidColorBrush b)
		{
			FontColor found = null;
			foreach (FontColor brush in this)
			{
				if (!brush.Brush.Color.Equals(b.Color))
				{
					continue;
				}
				found = brush;
				break;
			}
			return found;
		}

		public FontColor GetFontColorByName(string name)
		{
			FontColor found = null;
			foreach (FontColor b in this)
			{
				if (b.Name != name)
				{
					continue;
				}
				found = b;
				break;
			}
			return found;
		}

		public static int GetFontColorIndex(FontColor c)
		{
			AvailableColors brushList = new AvailableColors();
			int idx = 0;
			SolidColorBrush colorBrush = c.Brush;
			foreach (FontColor brush in brushList)
			{
				if (brush.Brush.Color.Equals(colorBrush.Color))
				{
					break;
				}
				idx++;
			}
			return idx;
		}

		private void Init()
		{
			PropertyInfo[] properties = typeof(Colors).GetProperties(BindingFlags.Static | BindingFlags.Public);
			for (int i = 0; i < (int)properties.Length; i++)
			{
				PropertyInfo prop = properties[i];
				string name = prop.Name;
				SolidColorBrush brush = new SolidColorBrush((Color)prop.GetValue(null, null));
				base.Add(new FontColor(name, brush));
			}
		}
	}
}

using System.IO;

namespace LLPlayer.Extensions;

public static class StringExtensions
{
    /// <summary>
    /// Split for various types of newline codes
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static IEnumerable<string> SplitToLines(this string? input)
    {
        if (input == null)
        {
            yield break;
        }

        using StringReader reader = new(input);

        string? line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }

    /// <summary>
    /// Convert only the first character to lower case
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static string ToLowerFirstChar(this string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        return char.ToLower(input[0]) + input.Substring(1);
    }

    /// <summary>
    /// Convert only the first character to upper case
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static string ToUpperFirstChar(this string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        return char.ToUpper(input[0]) + input.Substring(1);
    }
}

using System.Collections.Generic;

namespace FlyleafLib.MediaPlayer.Translation.Services;

public class TranslateServiceFactory
{
    private readonly Config.SubtitlesConfig _config;

    public TranslateServiceFactory(Config.SubtitlesConfig config)
    {
        _config = config;
    }

    /// <summary>
    /// GetService
    /// </summary>
    /// <param name="serviceType"></param>
    /// <param name="wordMode"></param>
    /// <returns></returns>
    /// <exception cref="TranslationConfigException"></exception>
    /// <exception cref="InvalidOperationException"></exception>
    public ITranslateService GetService(TranslateServiceType serviceType, bool wordMode)
    {
        switch (serviceType)
        {
            case TranslateServiceType.GoogleV1:
                return new GoogleV1TranslateService((GoogleV1TranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new GoogleV1TranslateSettings()));

            case TranslateServiceType.DeepL:
                return new DeepLTranslateService((DeepLTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new DeepLTranslateSettings()));

            case TranslateServiceType.DeepLX:
                return new DeepLXTranslateService((DeepLXTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new DeepLXTranslateSettings()));

            case TranslateServiceType.Ollama:
                return new OpenAIBaseTranslateService((OllamaTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OllamaTranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.LMStudio:
                return new OpenAIBaseTranslateService((LMStudioTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new LMStudioTranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.KoboldCpp:
                return new OpenAIBaseTranslateService((KoboldCppTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new KoboldCppTranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.OpenAI:
                return new OpenAIBaseTranslateService((OpenAITranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OpenAITranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.OpenAILike:
                return new OpenAIBaseTranslateService((OpenAILikeTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OpenAILikeTranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.Claude:
                return new OpenAIBaseTranslateService((ClaudeTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new ClaudeTranslateSettings()), _config.TranslateChatConfig, wordMode);

            case TranslateServiceType.LiteLLM:
                return new OpenAIBaseTranslateService((LiteLLMTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new LiteLLMTranslateSettings()), _config.TranslateChatConfig, wordMode);
        }

        throw new InvalidOperationException($"Translate service {serviceType} does not exist.");
    }
}

namespace FlyleafLib.MediaFramework.MediaStream;

public class ExternalVideoStream : ExternalStream
{
    public double   FPS             { get; set; }
    public int      Height          { get; set; }
    public int      Width           { get; set; }

    public bool     HasAudio        { get; set; }

    public string   DisplayMember =>
        $"{Width}x{Height} @{Math.Round(FPS, 2, MidpointRounding.AwayFromZero)} ({Codec}) [{Protocol}]{(HasAudio ? "" : " [NA]")}";
}

using System.Text.Json;
using System.Text.Json.Serialization;

namespace LLPlayer.Extensions;

/// <summary>
/// JsonConverter to serialize and deserialize interfaces with concrete types using a mapping between interfaces and concrete types
/// </summary>
/// <typeparam name="T"></typeparam>
public class JsonInterfaceConcreteConverter<T> : JsonConverter<T>
{
    private const string TypeKey = "TypeName";
    private readonly Dictionary<string, Type> _typeMapping;

    public JsonInterfaceConcreteConverter(Dictionary<string, Type> typeMapping)
    {
        _typeMapping = typeMapping;
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using JsonDocument jsonDoc = JsonDocument.ParseValue(ref reader);

        if (!jsonDoc.RootElement.TryGetProperty(TypeKey, out JsonElement typeProperty))
        {
            throw new JsonException("Type discriminator not found.");
        }

        string? typeDiscriminator = typeProperty.GetString();
        if (typeDiscriminator == null || !_typeMapping.TryGetValue(typeDiscriminator, out Type? targetType))
        {
            throw new JsonException($"Unknown type discriminator: {typeDiscriminator}");
        }

        // If a specific type is specified as the second argument, it is deserialized with that type
        return (T)JsonSerializer.Deserialize(jsonDoc.RootElement, targetType, options)!;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        Type type = value!.GetType();
        string typeDiscriminator = type.Name; // Use type name as discriminator

        // Serialize with concrete types, not interfaces
        string json = JsonSerializer.Serialize(value, type, options);
        using JsonDocument jsonDoc = JsonDocument.Parse(json);

        writer.WriteStartObject();
        // Save concrete type name
        writer.WriteString(TypeKey, typeDiscriminator);

        // Does this work even if it's nested?
        foreach (JsonProperty property in jsonDoc.RootElement.EnumerateObject())
        {
            property.WriteTo(writer);
        }

        writer.WriteEndObject();
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfColorFontDialog
{
    /// <summary>
    /// Interaction logic for ColorPicker.xaml
    /// </summary>
    public partial class ColorPicker : UserControl
    {
        private ColorPickerViewModel viewModel;

        public readonly static RoutedEvent ColorChangedEvent;

        public readonly static DependencyProperty SelectedColorProperty;

        public FontColor SelectedColor
        {
            get
            {
                FontColor fc = (FontColor)base.GetValue(ColorPicker.SelectedColorProperty) ?? AvailableColors.GetFontColor("Black");
                return fc;
            }
            set
            {
                this.viewModel.SelectedFontColor = value;
                base.SetValue(ColorPicker.SelectedColorProperty, value);
            }
        }

        static ColorPicker()
        {
            ColorPicker.ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColorPicker));
            ColorPicker.SelectedColorProperty = DependencyProperty.Register("SelectedColor", typeof(FontColor), typeof(ColorPicker), new UIPropertyMetadata(null));
        }
        public ColorPicker()
        {
            InitializeComponent();
            this.viewModel = new ColorPickerViewModel();
            base.DataContext = this.viewModel;
        }
        private void RaiseColorChangedEvent()
        {
            base.RaiseEvent(new RoutedEventArgs(ColorPicker.ColorChangedEvent));
        }

        private void superCombo_DropDownClosed(object sender, EventArgs e)
        {
            base.SetValue(ColorPicker.SelectedColorProperty, this.viewModel.SelectedFontColor);
            this.RaiseColorChangedEvent();
        }

        private void superCombo_Loaded(object sender, RoutedEventArgs e)
        {
            base.SetValue(ColorPicker.SelectedColorProperty, this.viewModel.SelectedFontColor);
        }

        public event RoutedEventHandler ColorChanged
        {
            add
            {
                base.AddHandler(ColorPicker.ColorChangedEvent, value);
            }
            remove
            {
                base.RemoveHandler(ColorPicker.ColorChangedEvent, value);
            }
        }
    }
}

using System.IO;
using FlyleafLib;
using Whisper.net.Ggml;

namespace LLPlayer.Services;

public class WhisperCppModelLoader
{
    public static List<WhisperCppModel> LoadAllModels()
    {
        WhisperConfig.EnsureModelsDirectory();

        List<WhisperCppModel> models = Enum.GetValues<GgmlType>()
            .Select(t => new WhisperCppModel { Model = t, })
            .ToList();

        foreach (WhisperCppModel model in models)
        {
            // Update download status
            string path = model.ModelFilePath;
            if (File.Exists(path))
            {
                model.Size = new FileInfo(path).Length;
            }
        }

        return models;
    }

    public static List<WhisperCppModel> LoadDownloadedModels()
    {
        return LoadAllModels()
            .Where(m => m.Downloaded)
            .ToList();
    }
}

using System;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfColorFontDialog
{
	public class FontInfo
	{
		public SolidColorBrush BrushColor
		{
			get;
			set;
		}

		public FontColor Color
		{
			get
			{
				return AvailableColors.GetFontColor(this.BrushColor);
			}
		}

		public FontFamily Family
		{
			get;
			set;
		}

		public double Size
		{
			get;
			set;
		}

		public FontStretch Stretch
		{
			get;
			set;
		}

		public FontStyle Style
		{
			get;
			set;
		}

		public FamilyTypeface Typeface
		{
			get
			{
				FamilyTypeface ftf = new FamilyTypeface()
				{
					Stretch = this.Stretch,
					Weight = this.Weight,
					Style = this.Style
				};
				return ftf;
			}
		}

		public FontWeight Weight
		{
			get;
			set;
		}

		public FontInfo()
		{
		}

		public FontInfo(FontFamily fam, double sz, FontStyle style, FontStretch strc, FontWeight weight, SolidColorBrush c)
		{
			this.Family = fam;
			this.Size = sz;
			this.Style = style;
			this.Stretch = strc;
			this.Weight = weight;
			this.BrushColor = c;
		}

		public static void ApplyFont(Control control, FontInfo font)
		{
			control.FontFamily = font.Family;
			control.FontSize = font.Size;
			control.FontStyle = font.Style;
			control.FontStretch = font.Stretch;
			control.FontWeight = font.Weight;
			control.Foreground = font.BrushColor;
		}

		public static FontInfo GetControlFont(Control control)
		{
			FontInfo font = new FontInfo()
			{
				Family = control.FontFamily,
				Size = control.FontSize,
				Style = control.FontStyle,
				Stretch = control.FontStretch,
				Weight = control.FontWeight,
				BrushColor = (SolidColorBrush)control.Foreground
			};
			return font;
		}

		public static string TypefaceToString(FamilyTypeface ttf)
		{
			StringBuilder sb = new StringBuilder(ttf.Stretch.ToString());
			sb.Append("-");
			sb.Append(ttf.Weight.ToString());
			sb.Append("-");
			sb.Append(ttf.Style.ToString());
			return sb.ToString();
		}
	}
}

using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace FlyleafLib.Plugins
{
    public class YoutubeDLJson : Format
    {
        // Remove not used as can cause issues with data types from time to time

        //public string   id              { get; set; }
        public string   title           { get; set; }
        //public string   description     { get; set; }
        //public string   upload_date     { get; set; }
        //public string   uploader        { get; set; }
        //public string   uploader_id     { get; set; }
        //public string   uploader_url    { get; set; }
        //public string   channel_id      { get; set; }
        //public string   channel_url     { get; set; }
        //public double   duration        { get; set; }
        //public double   view_count      { get; set; }
        //public double   average_rating  { get; set; }
        //public double   age_limit       { get; set; }
        public string   webpage_url     { get; set; }

        //public bool     playable_in_embed { get; set; }
        //public bool     is_live         { get; set; }
        //public bool     was_live        { get; set; }
        //public string   live_status     { get; set; }

        // Playlist
        public string   _type           { get; set; }
        public double   playlist_count  { get; set; }
        //public double   playlist_index  { get; set; }
        public string   playlist        { get; set; }
        public string   playlist_title  { get; set; }

        public Dictionary<string, List<SubtitlesFormat>>
                        automatic_captions { get; set; }
        //public List<string>
        //                categories      { get; set; }
        public List<Format>
                        formats         { get; set; }
        //public List<Thumbnail>
        //                thumbnails      { get; set; }
        public List<Chapter>
                        chapters
                                        { get; set; }
        //public double   like_count      { get; set; }
        //public double   dislike_count   { get; set; }
        //public string   channel         { get; set; }
        //public string   availability    { get; set; }
        //public string   webpage_url_basename
        //                                { get; set; }
        //public string   extractor       { get; set; }
        //public string   extractor_key   { get; set; }
        //public string   thumbnail       { get; set; }
        //public string   display_id      { get; set; }
        //public string   fulltitle       { get; set; }
        //public double   epoch           { get; set; }

        //public class DownloaderOptions
        //{
        //    public int http_chunk_size { get; set; }
        //}

        public class HttpHeaders
        {
            [JsonPropertyName("User-Agent")]
            public string UserAgent     { get; set; }

            [JsonPropertyName("Accept-Charset")]
            public string AcceptCharset { get; set; }
            public string Accept        { get; set; }

            [JsonPropertyName("Accept-Encoding")]
            public string AcceptEncoding{ get; set; }

            [JsonPropertyName("Accept-Language")]
            public string AcceptLanguage{ get; set; }
        }

        //public class Thumbnail
        //{
        //    public string url           { get; set; }
        //    public int    preference    { get; set; }
        //    public string id            { get; set; }
        //    public double height        { get; set; }
        //    public double width         { get; set; }
        //    public string resolution    { get; set; }
        //}

        public class SubtitlesFormat
        {
            public string ext   { get; set; }
            public string url   { get; set; }
            public string name  { get; set; }
        }
    }

    public class Chapter
    {
        public double start_time { get; set; }
        public double end_time   { get; set; }
        public string title      { get; set; }
    }

    public class Format
    {
        //public double   asr         { get; set; }
        //public double   filesize    { get; set; }
        //public string   format_id   { get; set; }
        //public string   format_note { get; set; }
        //public double   quality     { get; set; }
        public double   tbr         { get; set; }
        public string   url         { get; set; }
        public string   manifest_url{ get; set; }
        public string   language    { get; set; }
        //public int      language_preference
        //                            { get; set; }
        //public string   ext         { get; set; }
        public string   vcodec      { get; set; }
        public string   acodec      { get; set; }
        public double   abr         { get; set; }
        //public DownloaderOptions
        //                downloader_options { get; set; }
        //public string   container   { get; set; }
        public string   protocol    { get; set; }
        //public string   audio_ext   { get; set; }
        //public string   video_ext   { get; set; }
        public string   format      { get; set; }
        public Dictionary<string, string>
                        http_headers{ get; set; }
        public string   cookies     { get; set; }
        public double   fps         { get; set; }
        public double   height      { get; set; }
        public double   width       { get; set; }
        public double   vbr         { get; set; }
    }
}

using System;
using System.Windows.Input;

namespace FlyleafLib.Controls.WPF;

public class RelayCommand : ICommand
{
    private Action<object> execute;

    private Predicate<object> canExecute;

    private event EventHandler CanExecuteChangedInternal;

    public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute) { }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        this.execute    = execute ?? throw new ArgumentNullException("execute");
        this.canExecute = canExecute ?? throw new ArgumentNullException("canExecute");
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            CanExecuteChangedInternal += value;
        }

        remove
        {
            CommandManager.RequerySuggested -= value;
            CanExecuteChangedInternal -= value;
        }
    }

    private static bool DefaultCanExecute(object parameter) => true;
    public bool CanExecute(object parameter) => canExecute != null && canExecute(parameter);

    public void Execute(object parameter) => execute(parameter);

    public void OnCanExecuteChanged()
    {
        var handler = CanExecuteChangedInternal;
        handler?.Invoke(this, EventArgs.Empty);
         //CommandManager.InvalidateRequerySuggested();
    }

    public void Destroy()
    {
        canExecute = _ => false;
        execute = _ => { return; };
    }
}

using System.Windows;
using System.Windows.Controls;
using LLPlayer.ViewModels;

namespace LLPlayer.Resources;

public partial class PopupMenu : ResourceDictionary
{
    public PopupMenu()
    {
        InitializeComponent();
    }

    private void PopUpMenu_OnOpened(object sender, RoutedEventArgs e)
    {
        // TODO: L: should validate that the clipboard content is a video file?
        bool canPaste = !string.IsNullOrEmpty(Clipboard.GetText());
        MenuPasteUrl.IsEnabled = canPaste;

        // Don't hide the seek bar while displaying the context menu
        if (sender is ContextMenu menu && menu.DataContext is FlyleafOverlayVM vm)
        {
            vm.FL.Player.Activity.IsEnabled = false;
        }
    }

    private void PopUpMenu_OnClosed(object sender, RoutedEventArgs e)
    {
        if (sender is ContextMenu menu && menu.DataContext is FlyleafOverlayVM vm)
        {
            vm.FL.Player.Activity.IsEnabled = true;
        }
    }
}

using System.Text.RegularExpressions;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Controls.Settings;

public partial class SettingsVideo : UserControl
{
    public SettingsVideo()
    {
        InitializeComponent();
    }

    private void ValidationRatio(object sender, TextCompositionEventArgs e)
    {
        e.Handled = !Regex.IsMatch(e.Text, @"^[0-9\.\,\/\:]+$");
    }
}

using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace FlyleafLib;

public static partial class Utils
{
    public class ObservableDictionary<TKey, TVal> : Dictionary<TKey, TVal>, INotifyPropertyChanged, INotifyCollectionChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public new TVal this[TKey key]
        {
            get => base[key];

            set
            {
                if (ContainsKey(key) && base[key].Equals(value)) return;

                if (CollectionChanged != null)
                {
                    KeyValuePair<TKey, TVal> oldItem = new(key, base[key]);
                    KeyValuePair<TKey, TVal> newItem = new(key, value);
                    base[key] = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key.ToString()));
                    CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, this.ToList().IndexOf(newItem)));
                }
                else
                {
                    base[key] = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key.ToString()));
                }
            }
        }
    }
}

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace LLPlayer.Extensions;

public class Bindable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LLPlayer.Resources;

public partial class MaterialDesignMy : ResourceDictionary
{
    public MaterialDesignMy()
    {
        InitializeComponent();
    }

    /// <summary>
    /// Do not close the menu when right-clicking or CTRL+left-clicking on a context menu
    /// This is achieved by dynamically setting StaysOpenOnClick
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void MenuItem_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (sender is MenuItem menuItem)
        {
            if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) ||
                Mouse.RightButton == MouseButtonState.Pressed)
            {
                menuItem.StaysOpenOnClick = true;
            }
            else if (menuItem.StaysOpenOnClick)
            {
                menuItem.StaysOpenOnClick = false;
            }
        }
    }
}

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Media;

namespace WpfColorFontDialog
{
	internal class ColorPickerViewModel : INotifyPropertyChanged
	{
		private ReadOnlyCollection<FontColor> roFontColors;

		private FontColor selectedFontColor;

		public ReadOnlyCollection<FontColor> FontColors
		{
			get
			{
				return this.roFontColors;
			}
		}

		public FontColor SelectedFontColor
		{
			get
			{
				return this.selectedFontColor;
			}
			set
			{
				if (this.selectedFontColor == value)
				{
					return;
				}
				this.selectedFontColor = value;
				this.OnPropertyChanged("SelectedFontColor");
			}
		}

		public ColorPickerViewModel()
		{
			this.selectedFontColor = AvailableColors.GetFontColor(Colors.Black);
			this.roFontColors = new ReadOnlyCollection<FontColor>(new AvailableColors());
		}

		private void OnPropertyChanged(string propertyName)
		{
			if (this.PropertyChanged != null)
			{
				this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;
	}
}

using System.Windows;
using System.Windows.Media;

namespace LLPlayer.Extensions;

public static class UIHelper
{
    // ref: https://www.infragistics.com/community/blogs/b/blagunas/posts/find-the-parent-control-of-a-specific-type-in-wpf-and-silverlight
    public static T? FindParent<T>(DependencyObject child) where T : DependencyObject
    {
        //get parent item
        DependencyObject? parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null)
            return null;

        //check if the parent matches the type we're looking for
        if (parentObject is T parent)
            return parent;

        return FindParent<T>(parentObject);
    }

    /// <summary>
    /// Traverses the visual tree upward from the current element to determine if an element with the specified name exists.j
    /// </summary>
    /// <param name="element">Element to start with (current element)</param>
    /// <param name="name">Name of the element</param>
    /// <returns>True if the element with the specified name exists, false otherwise.</returns>
    public static bool FindParentWithName(DependencyObject? element, string name)
    {
        if (element == null)
        {
            return false;
        }

        DependencyObject? current = element;

        while (current != null)
        {
            if (current is FrameworkElement fe && fe.Name == name)
            {
                return true;
            }

            current = VisualTreeHelper.GetParent(current);
        }

        return false;
    }
}

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace LLPlayer.Extensions;

public static class Guards
{
    /// Throws an <see cref="InvalidOperationException"/> immediately.
    /// <param name="message">error message</param>
    /// <exception cref="InvalidOperationException"></exception>
    public static void Fail(string? message = null)
    {
        throw new InvalidOperationException(message);
    }

    /// Throws an <see cref="InvalidOperationException"/> if <paramref name="variable"/> is null.
    /// <param name="variable">The reference type variable to validate as non-null.</param>
    /// <param name="variableName">The name of the variable with which <paramref name="variable"/> corresponds.</param>
    /// <exception cref="InvalidOperationException"></exception>
    public static void ThrowIfNull([NotNull] object? variable, [CallerArgumentExpression(nameof(variable))] string? variableName = null)
    {
        if (variable is null)
        {
            throw new InvalidOperationException(variableName);
        }
    }
}

using FluentAssertions;

namespace FlyleafLib;

public class SubtitleTextUtilTests
{
    [Theory]
    [InlineData("", "")]
    [InlineData("   ", "   ")] // Assume trim is done beforehand
    [InlineData("Hello", "Hello")]
    [InlineData("Hello\nWorld", "Hello World")]
    [InlineData("Hello\r\nWorld", "Hello World")]
    [InlineData("Hello\n\nWorld", "Hello World")]
    [InlineData("Hello\r\n\r\nWorld", "Hello World")]
    [InlineData("Hello\n  \nWorld", "Hello    World")]

    [InlineData("- Hello\n- How are you?", "- Hello\n- How are you?")]
    [InlineData("- Hello\n - How are you?", "- Hello  - How are you?")]
    [InlineData("- Hello\r- How are you?", "- Hello\r- How are you?")]
    [InlineData("- Hello\n\n- How are you?", "- Hello\n\n- How are you?")]
    [InlineData("- Hello\r\n- How are you?", "- Hello\r\n- How are you?")]
    [InlineData("- Hello\nWorld", "- Hello World")]
    [InlineData("- こんにちは\n- 世界", "- こんにちは\n- 世界")]
    [InlineData("- こんにちは\n世界", "- こんにちは 世界")]

    [InlineData("こんにちは\n世界", "こんにちは 世界")]
    [InlineData("🙂\n🙃", "🙂 🙃")]
    [InlineData("Hello\nWorld", "Hello World")]

    [InlineData("- Hello\n- Good\nbye", "- Hello\n- Good bye")]
    [InlineData("- Hello\nWorld\n- Good\nbye", "- Hello World\n- Good bye")]

    [InlineData("Hello\n- Good\n- bye", "Hello - Good - bye")]
    [InlineData(" -Hello\n- Good\n- bye", " -Hello - Good - bye")]

    [InlineData("- Hello\n- aa-bb-cc dd", "- Hello\n- aa-bb-cc dd")]
    [InlineData("- Hello\naa-bb-cc dd", "- Hello aa-bb-cc dd")]

    [InlineData("- Hello\n- Goodbye", "- Hello\n- Goodbye")] // hyphen
    [InlineData("– Hello\n– Goodbye", "– Hello\n– Goodbye")] // en dash
    [InlineData("- Hello\n– Goodbye", "- Hello – Goodbye")] // hyphen + en dash

    public void FlattenUnlessAllDash_ShouldReturnExpected(string input, string expected)
    {
        string result = SubtitleTextUtil.FlattenText(input);
        result.Should().Be(expected);
    }
}

using Vortice.Direct3D11;

using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;

namespace FlyleafLib.MediaFramework.MediaFrame;

public unsafe class VideoFrame : FrameBase
{
    public ID3D11Texture2D[]            textures;       // Planes
    public ID3D11ShaderResourceView[]   srvs;           // Views

    // Zero-Copy
    public AVFrame*                     avFrame;        // Lets ffmpeg to know that we still need it
}

using System.Windows;

namespace LLPlayer.Controls;

public class WordClickedEventArgs(RoutedEvent args) : RoutedEventArgs(args)
{
    public required MouseClick Mouse { get; init; }
    public required string Words { get; init; }
    public required bool IsWord { get; init; }
    public required string Text { get; init; }
    public required bool IsTranslated { get; init; }
    public required int SubIndex { get; init; }
    public required int WordOffset { get; init; }

    // For screen subtitles
    public double WordsX { get; init; }
    public double WordsWidth { get; init; }

    // For sidebar subtitles
    public FrameworkElement? Sender { get; init; }
}

public enum MouseClick
{
    Left, Right, Middle
}

public delegate void WordClickedEventHandler(object sender, WordClickedEventArgs e);

using System;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;

namespace WpfColorFontDialog
{
	public class FontColor
	{
		public SolidColorBrush Brush
		{
			get;
			set;
		}

		public string Name
		{
			get;
			set;
		}

		public FontColor(string name, SolidColorBrush brush)
		{
			this.Name = name;
			this.Brush = brush;
		}

		public override bool Equals(object obj)
		{
			if (obj == null)
			{
				return false;
			}
			FontColor p = obj as FontColor;
			if (p == null)
			{
				return false;
			}
			if (this.Name != p.Name)
			{
				return false;
			}
			return this.Brush.Equals(p.Brush);
		}

		public bool Equals(FontColor p)
		{
			if (p == null)
			{
				return false;
			}
			if (this.Name != p.Name)
			{
				return false;
			}
			return this.Brush.Equals(p.Brush);
		}

		public override int GetHashCode()
		{
			return base.GetHashCode();
		}

		public override string ToString()
		{
			string[] name = new string[] { "FontColor [Color=", this.Name, ", ", this.Brush.ToString(), "]" };
			return string.Concat(name);
		}
	}
}

namespace FlyleafLib.MediaFramework.MediaPlaylist;

public class Session
{
    public string   Url                     { get; set; }
    public int      PlaylistItem            { get; set; } = -1;

    public int      ExternalAudioStream     { get; set; } = -1;
    public int      ExternalVideoStream     { get; set; } = -1;
    public string   ExternalSubtitlesUrl    { get; set; }

    public int      AudioStream             { get; set; } = -1;
    public int      VideoStream             { get; set; } = -1;
    public int      SubtitlesStream         { get; set; } = -1;

    public long     CurTime                 { get; set; }

    public long     AudioDelay              { get; set; }
    public long     SubtitlesDelay          { get; set; }

    internal bool   isReopen; // temp fix for opening existing playlist item as a new session (should not re-initialize - is like switch)

    //public SavedSession() { }
    //public SavedSession(int extVideoStream, int videoStream, int extAudioStream, int audioStream, int extSubtitlesStream, int subtitlesStream, long curTime, long audioDelay, long subtitlesDelay)
    //{
    //    Update(extVideoStream, videoStream, extAudioStream, audioStream, extSubtitlesStream, subtitlesStream, curTime, audioDelay, subtitlesDelay);
    //}
    //public void Update(int extVideoStream, int videoStream, int extAudioStream, int audioStream, int extSubtitlesStream, int subtitlesStream, long curTime, long audioDelay, long subtitlesDelay)
    //{
    //    ExternalVideoStream = extVideoStream; VideoStream = videoStream;
    //    ExternalAudioStream = extAudioStream; AudioStream = audioStream;
    //    ExternalSubtitlesStream = extSubtitlesStream; SubtitlesStream = subtitlesStream;
    //    CurTime = curTime; AudioDelay = audioDelay; SubtitlesDelay = subtitlesDelay;
    //}
}

using System.Collections.Generic;

namespace FlyleafLib.MediaFramework.MediaDevice;

public class DeviceBase :DeviceBase<DeviceStreamBase>
{
    public DeviceBase(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink)
    {
    }
}

public class DeviceBase<T>
    where T: DeviceStreamBase
{
    public string   FriendlyName            { get; }
    public string   SymbolicLink            { get; }
    public IList<T>
                    Streams                 { get; protected set; }
    public string   Url                     { get; protected set; } // default Url

    public DeviceBase(string friendlyName, string symbolicLink)
    {
        FriendlyName = friendlyName;
        SymbolicLink = symbolicLink;

        Engine.Log.Debug($"[{(this is AudioDevice ? "Audio" : "Video")}Device] {friendlyName}");
    }

    public override string ToString() => FriendlyName;
}

using System;
using Vortice.MediaFoundation;

namespace FlyleafLib.MediaFramework.MediaDevice;

public class AudioDevice : DeviceBase<AudioDeviceStream>
{
    public AudioDevice(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink)
        => Url = $"fmt://dshow?audio={FriendlyName}";

    public static void RefreshDevices()
    {
        Utils.UIInvokeIfRequired(() =>
        {
            Engine.Audio.CapDevices.Clear();

            var devices = MediaFactory.MFEnumAudioDeviceSources();
                foreach (var device in devices)
                    try { Engine.Audio.CapDevices.Add(new AudioDevice(device.FriendlyName, device.SymbolicLink)); } catch(Exception) { }
        });
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings;

public partial class SettingsPlayer : UserControl
{
    public SettingsPlayer()
    {
        InitializeComponent();
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings;

public partial class SettingsMouse : UserControl
{
    public SettingsMouse()
    {
        InitializeComponent();
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings;

public partial class SettingsKeysOffset : UserControl
{
    public SettingsKeysOffset()
    {
        InitializeComponent();
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings;

public partial class SettingsSubtitlesPS : UserControl
{
    public SettingsSubtitlesPS()
    {
        InitializeComponent();
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings.TransControl;

public partial class OpenAIBaseTranslateControl : UserControl
{
    public OpenAIBaseTranslateControl()
    {
        InitializeComponent();
    }
}

using System.Windows.Controls;

namespace LLPlayer.Controls.Settings;

// TODO: L: Allow manual setting of text color
// TODO: L: Allow setting of background color and video background color

public partial class SettingsThemes : UserControl
{
    public SettingsThemes()
    {
        InitializeComponent();
    }
}

namespace FlyleafLib;

/// <summary>
/// Anonymous Disposal Pattern
/// </summary>
public class Disposable : IDisposable
{
    public static Disposable Create(Action onDispose) => new(onDispose);

    public static Disposable Empty { get; } = new(null);

    Action _onDispose;
    Disposable(Action onDispose) => _onDispose = onDispose;

    public void Dispose()
    {
        _onDispose?.Invoke();
        _onDispose = null;
    }
}

using FlyleafLib.MediaFramework.MediaDemuxer;

namespace FlyleafLib.MediaFramework.MediaStream;

public unsafe class DataStream : StreamBase
{

    public DataStream() { }
    public DataStream(Demuxer demuxer, AVStream* st) : base(demuxer, st)
    {
        Demuxer = demuxer;
        AVStream = st;
        Refresh();
    }

    public override void Refresh()
    {
        base.Refresh();
    }

    public override string GetDump()
        => $"[{Type} #{StreamIndex}] {CodecID}";
}

using System;
using System.Windows.Input;

namespace FlyleafLib.Controls.WPF;

public class RelayCommandSimple : ICommand
{
    public event EventHandler CanExecuteChanged { add { } remove { } }
    Action execute;

    public RelayCommandSimple(Action execute)   => this.execute = execute;
    public bool CanExecute(object parameter)    => true;
    public void Execute(object parameter)       => execute();
}

namespace FlyleafLib.MediaFramework.MediaDevice;

public class DeviceStreamBase
{
    public string   DeviceFriendlyName  { get; }
    public string   Url                 { get; protected set; }

    public DeviceStreamBase(string deviceFriendlyName) => DeviceFriendlyName = deviceFriendlyName;
}

using System.Collections.Generic;
using System.IO;

namespace FlyleafLib.MediaFramework.MediaDemuxer;

public class DemuxerInput : NotifyPropertyChanged
{
    /// <summary>
    /// Url provided as a demuxer input
    /// </summary>
    public string   Url             { get => _Url; set => _Url = Utils.FixFileUrl(value); }
    string _Url;

    /// <summary>
    /// Fallback url provided as a demuxer input
    /// </summary>
    public string   UrlFallback     { get => _UrlFallback; set => _UrlFallback = Utils.FixFileUrl(value); }
    string _UrlFallback;

    /// <summary>
    /// IOStream provided as a demuxer input
    /// </summary>
    public Stream   IOStream        { get; set; }

    public Dictionary<string, string>
                    HTTPHeaders     { get; set; }

    public string   UserAgent       { get; set; }

    public string   Referrer        { get; set; }
}

using System;

namespace FlyleafLib.MediaFramework.MediaFrame;

public class AudioFrame : FrameBase
{
    public IntPtr   dataPtr;
    public int      dataLen;
}

namespace FlyleafLib.MediaFramework.MediaStream;

public class ExternalAudioStream : ExternalStream
{
    public int      SampleRate      { get; set; }
    public string   ChannelLayout   { get; set; }
    public Language Language        { get; set; }

    public bool     HasVideo        { get; set; }
}

namespace FlyleafLib.Controls;

public interface IHostPlayer
{
    bool Player_CanHideCursor();
    bool Player_GetFullScreen();
    void Player_SetFullScreen(bool value);
    void Player_Disposed();
}

namespace FlyleafLib.MediaFramework.MediaFrame;

public class DataFrame : FrameBase
{
    public AVCodecID DataCodecId;
    public byte[] Data;
}

// For some reason, if these are not imported, it cannot be built with anything other than portable for target runtime.
// Prism import errors occur, so work around this.
global using Prism;
global using Prism.Commands;
global using Prism.DryIoc;
global using Prism.Dialogs;
global using Prism.Ioc;

namespace FlyleafLib.MediaFramework.MediaDevice;

public class AudioDeviceStream : DeviceStreamBase
{
    public AudioDeviceStream(string deviceName) : base(deviceName) { }
}

namespace FlyleafLib.MediaFramework.MediaFrame;

public unsafe class FrameBase
{
    public long         timestamp;
    //public long         pts;
}

public
partial
class
MainWindow
: Window
{
public MainWindow () {}
private void SetWindowSize () {}
[DllImport("DwmApi")] private static extern int DwmSetWindowAttribute (IntPtr hwnd, int attr, int[] attrValue, int attrSize) {}
const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
public static void SetTitleBarDarkMode (Window window) {}
}

This code segment functions to: Initialize the main application window with dynamic size selection based on available screen dimensions and configure visual theme settings for the title bar according to system capabilities and user preferences.

```c_sharp
[TASK_BEGIN]
public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = new MainViewModel();
        SetWindowSize();
    }

    private void SetWindowSize()
    {
        Width = 1280;
        Height = 720; // 16:9 aspect ratio
    }

    [DllImport("DwmApi")]
    private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);

    const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

    public static void SetTitleBarDarkMode(Window window)
    {
        var handle = new WindowInteropHelper(window).EnsureHandle();
        int[] useDarkMode = { 1 };

        if (Environment.OSVersion.Version.Build >= 17763)
        {
            DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, useDarkMode, sizeof(int));
        }

        if (Application.Current.Resources["DarkBackground"] is System.Windows.Media.Brush darkBrush)
        {
            window.Background = darkBrush;
        }
    }
}
[TASK_END]
```

public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = ((App)Application.Current).Container.Resolve<MainWindowVM>();
        InitializeComponent();
        SetWindowSize();
        SetTitleBarDarkMode(this);
    }
    private void SetWindowSize()
    {
        List<Size> candidateSizes =
        [
            new(1280, 720),
            new(1024, 576),
            new(960, 540),
            new(800, 450),
            new(640, 360),
            new(480, 270),
            new(320, 180)
        ];
        double availableWidth = SystemParameters.WorkArea.Width;
        double availableHeight = SystemParameters.WorkArea.Height;
        Size selectedSize = candidateSizes.FirstOrDefault(
            s => s.Width <= availableWidth && s.Height <= availableHeight,
            candidateSizes[^1]);
        Width = selectedSize.Width;
        Height = selectedSize.Height;
    }
    #region Dark Title Bar
    [DllImport("DwmApi")]
    private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);
    const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
    public static void SetTitleBarDarkMode(Window window)
    {
        if (!(Environment.OSVersion.Version >= new Version(10, 0, 18985)))
        {
            return;
        }
        var fl = ((App)Application.Current).Container.Resolve<FlyleafManager>();
        if (!fl.Config.IsDarkTitlebar)
        {
            return;
        }
        bool darkMode = true;
        IntPtr hWnd = new WindowInteropHelper(window).EnsureHandle();
        DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, [darkMode ? 1 : 0], 4);
    }
    #endregion
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = new MainViewModel();
        SetWindowSize();
    }
    private void SetWindowSize()
    {
        Width = 1280;
        Height = 720;
    }
    [DllImport("DwmApi")]
    private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);
    const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
    public static void SetTitleBarDarkMode(Window window)
    {
        var handle = new WindowInteropHelper(window).EnsureHandle();
        int[] useDarkMode = { 1 };
        if (Environment.OSVersion.Version.Build >= 17763)
        {
            DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, useDarkMode, sizeof(int));
        }
        if (Application.Current.Resources["DarkBackground"] is System.Windows.Media.Brush darkBrush)
        {
            window.Background = darkBrush;
        }
    }
}